From fb763d9c6f2f8591342c00e77aa52e2b1493dda5 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 19 Oct 2023 16:50:29 +0800 Subject: [PATCH 01/83] added zip suport Signed-off-by: Patrick Zheng --- cmd/notation/main.go | 3 +- cmd/notation/plugin/cmd.go | 30 +++ cmd/notation/plugin/install.go | 221 +++++++++++++++++++++ cmd/notation/{plugin.go => plugin/list.go} | 11 +- internal/osutil/file.go | 68 +++++++ 5 files changed, 322 insertions(+), 11 deletions(-) create mode 100644 cmd/notation/plugin/cmd.go create mode 100644 cmd/notation/plugin/install.go rename cmd/notation/{plugin.go => plugin/list.go} (91%) 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..17f3d3150 --- /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( + pluginListCommand(), + pluginInstallCommand(nil), + ) + + return command +} diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go new file mode 100644 index 000000000..5729da746 --- /dev/null +++ b/cmd/notation/plugin/install.go @@ -0,0 +1,221 @@ +// 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 ( + "archive/zip" + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/plugin/proto" + "github.com/notaryproject/notation/internal/osutil" + "github.com/opencontainers/go-digest" + "github.com/spf13/cobra" +) + +const ( + TypeZip = "application/zip" + TypeGzip = "application/x-gzip" +) + +type pluginInstallOpts struct { + inputPath string + inputCheckSum string + forced bool +} + +func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { + if opts == nil { + opts = &pluginInstallOpts{} + } + command := &cobra.Command{ + Use: "install [flags] ", + Short: "Install plugin", + Long: `Install a Notation plugin + +Example - Install plugin from file system: + notation plugin install myPlugin.zip +`, + RunE: func(cmd *cobra.Command, args []string) error { + return installPlugin(cmd, opts) + }, + } + command.Flags().StringVar(&opts.inputPath, "file", "", "file path of the plugin to be installed, only supports tar.gz or zip format") + command.Flags().StringVar(&opts.inputCheckSum, "checksum", "", "if set, must match the SHA256 of the plugin tar.gz/zip to be installed") + command.Flags().BoolVar(&opts.forced, "forced", false, "do not force to install and overwrite the plugin") + command.MarkFlagRequired("file") + return command +} + +func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { + inputPath := opts.inputPath + // sanity check + iputFileStat, err := os.Stat(inputPath) + if err != nil { + return fmt.Errorf("failed to install the plugin, %w", err) + } + if !iputFileStat.Mode().IsRegular() { + return fmt.Errorf("failed to install the plugin, %s is not a regular file", inputPath) + } + // checkSum check + if opts.inputCheckSum != "" { + if err := validateCheckSum(inputPath, opts.inputCheckSum); err != nil { + return fmt.Errorf("failed to install the plugin, %w", err) + } + } + // install the plugin based on file type + fileType, err := osutil.DetectFileType(inputPath) + if err != nil { + return fmt.Errorf("failed to install the plugin, %w", err) + } + switch fileType { + case TypeZip: + if err := installPluginFromZip(command.Context(), inputPath, opts.forced); err != nil { + return fmt.Errorf("failed to install the plugin, %w", err) + } + default: + return errors.New("failed to install the plugin, invalid file type. Only support tar.gz and zip") + } + return nil +} + +// validateCheckSum returns nil if SHA256 of file at path equals to checkSum. +func validateCheckSum(path string, checkSum string) error { + r, err := os.Open(path) + if err != nil { + return err + } + defer r.Close() + dgst, err := digest.FromReader(r) + if err != nil { + return err + } + if dgst.Encoded() != checkSum { + return errors.New("plugin checkSum does not match user input") + } + return nil +} + +// installPluginFromZip extracts a plugin zip file, validate and +// install the plugin +func installPluginFromZip(ctx context.Context, zipPath string, forced bool) error { + archive, err := zip.OpenReader(zipPath) + if err != nil { + return err + } + defer archive.Close() + tmpDir, err := os.MkdirTemp(".", "unzipTmpDir") + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + for _, f := range archive.File { + fileMode := f.Mode() + // only consider regular executable files in the zip + if fileMode.IsRegular() && osutil.IsOwnerExecutalbeFile(fileMode) { + pluginName, err := getPluginNameFromExecutableFileName(f.Name) + // if error is not nil, continue to next file + // if error is nil, we find the plugin executable file + if err == nil { + // check plugin existence + if !forced { + existed, err := checkPluginExistence(ctx, pluginName) + if err != nil { + return fmt.Errorf("failed to check plugin existence, %w", err) + } + if existed { + return fmt.Errorf("plugin %s already existed", pluginName) + } + } + // extract to tmp dir + tmpFilePath := filepath.Join(tmpDir, filepath.Base(f.Name)) + pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + fileInArchive, err := f.Open() + if err != nil { + return err + } + defer fileInArchive.Close() + if _, err := io.Copy(pluginFile, fileInArchive); err != nil { + return err + } + if err := pluginFile.Close(); err != nil { + return err + } + // validate plugin metadata + if err := validatePluginMetadata(ctx, pluginName, tmpFilePath); err != nil { + return err + } + // install plugin + pluginPath, err := dir.PluginFS().SysPath(pluginName) + if err != nil { + return nil + } + _, err = osutil.CopyToDir(tmpFilePath, pluginPath) + if err != nil { + return err + } + fmt.Printf("Succussefully installed plugin %s\n", pluginName) + return nil + } + } + } + return errors.New("plugin executable file not found in zip") +} + +// getPluginNameFromExecutableFileName gets plugin name from plugin executable +// file name based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation +func getPluginNameFromExecutableFileName(execFileName string) (string, error) { + fileName := osutil.FileNameWithoutExtension(execFileName) + _, pluginName, found := strings.Cut(fileName, "-") + if !found || !strings.HasPrefix(fileName, proto.Prefix) { + return "", fmt.Errorf("invalid plugin executable file name. file name requires format notation-{plugin-name}, got %s", fileName) + } + return pluginName, nil +} + +// checkPluginExistence returns true if a plugin already exists +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) { + return false, nil + } + return false, err + } + return true, nil +} + +// validatePluginMetadata validates plugin metadata before installation +func validatePluginMetadata(ctx context.Context, pluginName, path string) error { + plugin, err := plugin.NewCLIPlugin(ctx, pluginName, path) + if err != nil { + return err + } + _, err = plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) + if err != nil { + return err + } + return nil +} diff --git a/cmd/notation/plugin.go b/cmd/notation/plugin/list.go similarity index 91% rename from cmd/notation/plugin.go rename to cmd/notation/plugin/list.go index be2c2f3ad..a6ae1a504 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,15 +24,6 @@ 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 { return &cobra.Command{ Use: "list [flags]", diff --git a/internal/osutil/file.go b/internal/osutil/file.go index 70fdfdf8c..0c407f0ae 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -17,8 +17,10 @@ import ( "fmt" "io" "io/fs" + "net/http" "os" "path/filepath" + "strings" ) // WriteFile writes to a path with all parent directories created. @@ -94,3 +96,69 @@ func IsRegularFile(path string) (bool, error) { return fileStat.Mode().IsRegular(), nil } + +// DetectFileType returns a file's content type given path +func DetectFileType(path string) (string, error) { + f, err := os.ReadFile(path) + if err != nil { + return "", err + } + return http.DetectContentType(f), nil +} + +// ExtractTarGz decompress and untar a tar.gz file to destination directory, +// all files in the tar.gz must be regular files. +// func ExtractTarGz(tarGzPath string, dstDir string) error { +// r, err := os.Open(tarGzPath) +// if err != nil { +// return err +// } +// defer r.Close() +// uncompressedStream, err := gzip.NewReader(r) +// if err != nil { +// return err +// } +// defer uncompressedStream.Close() +// tarReader := tar.NewReader(uncompressedStream) +// if err := os.MkdirAll(dstDir, 0700); err != nil { +// return err +// } +// for { +// header, err := tarReader.Next() +// if err != nil { +// if err == io.EOF { +// break +// } +// return err +// } +// switch header.Typeflag { +// case tar.TypeReg: +// filePath := filepath.Join(dstDir, header.Name) +// dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode()) +// if err != nil { +// return err +// } +// defer dstFile.Close() +// if _, err := io.Copy(dstFile, tarReader); err != nil { +// return err +// } +// default: +// return errors.New("regular file required") +// } +// } +// return nil +// } + +// FileNameWithoutExtension returns the file name without extension. +// For example, +// when input is xyz.exe, output is xyz +// when input is xyz.tar.gz, output is xyz.tar +func FileNameWithoutExtension(inputName string) string { + fileName := filepath.Base(inputName) + return strings.TrimSuffix(fileName, filepath.Ext(fileName)) +} + +// IsOwnerExecutalbeFile checks whether file is owner executable +func IsOwnerExecutalbeFile(mode fs.FileMode) bool { + return mode&0100 != 0 +} From c326db28bfe4dc737e2a6cd6262fcb4f16f7b5fd Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 20 Oct 2023 11:39:03 +0800 Subject: [PATCH 02/83] added tar.gz support Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 93 +++++++++++++++++++++++++++++++--- internal/osutil/file.go | 43 ---------------- 2 files changed, 87 insertions(+), 49 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 5729da746..4337e0071 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -14,7 +14,9 @@ package plugin import ( + "archive/tar" "archive/zip" + "compress/gzip" "context" "errors" "fmt" @@ -91,6 +93,10 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { if err := installPluginFromZip(command.Context(), inputPath, opts.forced); err != nil { return fmt.Errorf("failed to install the plugin, %w", err) } + case TypeGzip: + if err := installPluginFromTarGz(command.Context(), inputPath, opts.forced); err != nil { + return fmt.Errorf("failed to install the plugin, %w", err) + } default: return errors.New("failed to install the plugin, invalid file type. Only support tar.gz and zip") } @@ -114,8 +120,8 @@ func validateCheckSum(path string, checkSum string) error { return nil } -// installPluginFromZip extracts a plugin zip file, validate and -// install the plugin +// installPluginFromZip extracts a plugin zip file, validates and +// installs the plugin func installPluginFromZip(ctx context.Context, zipPath string, forced bool) error { archive, err := zip.OpenReader(zipPath) if err != nil { @@ -131,8 +137,7 @@ func installPluginFromZip(ctx context.Context, zipPath string, forced bool) erro fileMode := f.Mode() // only consider regular executable files in the zip if fileMode.IsRegular() && osutil.IsOwnerExecutalbeFile(fileMode) { - pluginName, err := getPluginNameFromExecutableFileName(f.Name) - // if error is not nil, continue to next file + pluginName, err := extractPluginNameFromExecutableFileName(f.Name) // if error is nil, we find the plugin executable file if err == nil { // check plugin existence @@ -183,9 +188,85 @@ func installPluginFromZip(ctx context.Context, zipPath string, forced bool) erro return errors.New("plugin executable file not found in zip") } -// getPluginNameFromExecutableFileName gets plugin name from plugin executable +// installPluginFromTarGz extracts and untar a plugin tar.gz file, validates and +// installs the plugin +func installPluginFromTarGz(ctx context.Context, tarGzPath string, forced bool) error { + r, err := os.Open(tarGzPath) + if err != nil { + return err + } + defer r.Close() + decompressedStream, err := gzip.NewReader(r) + if err != nil { + return err + } + defer decompressedStream.Close() + tarReader := tar.NewReader(decompressedStream) + tmpDir, err := os.MkdirTemp(".", "untarGzTmpDir") + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + for { + header, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + return err + } + fileMode := header.FileInfo().Mode() + // only consider regular executable files + if fileMode.IsRegular() && osutil.IsOwnerExecutalbeFile(fileMode) { + pluginName, err := extractPluginNameFromExecutableFileName(header.Name) + // if error is nil, we find the plugin executable file + if err == nil { + // check plugin existence + if !forced { + existed, err := checkPluginExistence(ctx, pluginName) + if err != nil { + return fmt.Errorf("failed to check plugin existence, %w", err) + } + if existed { + return fmt.Errorf("plugin %s already existed", pluginName) + } + } + // extract to tmp dir + tmpFilePath := filepath.Join(tmpDir, header.Name) + pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode()) + if err != nil { + return err + } + if _, err := io.Copy(pluginFile, tarReader); err != nil { + return err + } + if err := pluginFile.Close(); err != nil { + return err + } + // validate plugin metadata + if err := validatePluginMetadata(ctx, pluginName, tmpFilePath); err != nil { + return err + } + // install plugin + pluginPath, err := dir.PluginFS().SysPath(pluginName) + if err != nil { + return nil + } + _, err = osutil.CopyToDir(tmpFilePath, pluginPath) + if err != nil { + return err + } + fmt.Printf("Succussefully installed plugin %s\n", pluginName) + return nil + } + } + } + return nil +} + +// extractPluginNameFromExecutableFileName gets plugin name from plugin executable // file name based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation -func getPluginNameFromExecutableFileName(execFileName string) (string, error) { +func extractPluginNameFromExecutableFileName(execFileName string) (string, error) { fileName := osutil.FileNameWithoutExtension(execFileName) _, pluginName, found := strings.Cut(fileName, "-") if !found || !strings.HasPrefix(fileName, proto.Prefix) { diff --git a/internal/osutil/file.go b/internal/osutil/file.go index 0c407f0ae..c75915e7e 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -106,49 +106,6 @@ func DetectFileType(path string) (string, error) { return http.DetectContentType(f), nil } -// ExtractTarGz decompress and untar a tar.gz file to destination directory, -// all files in the tar.gz must be regular files. -// func ExtractTarGz(tarGzPath string, dstDir string) error { -// r, err := os.Open(tarGzPath) -// if err != nil { -// return err -// } -// defer r.Close() -// uncompressedStream, err := gzip.NewReader(r) -// if err != nil { -// return err -// } -// defer uncompressedStream.Close() -// tarReader := tar.NewReader(uncompressedStream) -// if err := os.MkdirAll(dstDir, 0700); err != nil { -// return err -// } -// for { -// header, err := tarReader.Next() -// if err != nil { -// if err == io.EOF { -// break -// } -// return err -// } -// switch header.Typeflag { -// case tar.TypeReg: -// filePath := filepath.Join(dstDir, header.Name) -// dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode()) -// if err != nil { -// return err -// } -// defer dstFile.Close() -// if _, err := io.Copy(dstFile, tarReader); err != nil { -// return err -// } -// default: -// return errors.New("regular file required") -// } -// } -// return nil -// } - // FileNameWithoutExtension returns the file name without extension. // For example, // when input is xyz.exe, output is xyz From d19ec82d90a9459defa279cfb18a85de322a9fa2 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 23 Oct 2023 15:18:33 +0800 Subject: [PATCH 03/83] updated Signed-off-by: Patrick Zheng --- cmd/notation/internal/errors/errors.go | 13 ++ cmd/notation/plugin/install.go | 238 ++++++++++++------------- internal/osutil/file.go | 8 +- 3 files changed, 131 insertions(+), 128 deletions(-) diff --git a/cmd/notation/internal/errors/errors.go b/cmd/notation/internal/errors/errors.go index bb06e6f6f..631e4107e 100644 --- a/cmd/notation/internal/errors/errors.go +++ b/cmd/notation/internal/errors/errors.go @@ -50,3 +50,16 @@ type ErrorExceedMaxSignatures struct { func (e ErrorExceedMaxSignatures) Error() string { return fmt.Sprintf("exceeded configured limit of max signatures %d to examine", e.MaxSignatures) } + +// ErrorInvalidPluginName is used when a plugin executable file name does not +// follow the spec. +type ErrorInvalidPluginName struct { + Msg string +} + +func (e ErrorInvalidPluginName) Error() string { + if e.Msg != "" { + return e.Msg + } + return "invalid plugin file name" +} diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 4337e0071..524072d45 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -21,13 +21,17 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "strings" "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/log" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/proto" + notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" + "github.com/notaryproject/notation/internal/cmd" "github.com/notaryproject/notation/internal/osutil" "github.com/opencontainers/go-digest" "github.com/spf13/cobra" @@ -39,9 +43,10 @@ const ( ) type pluginInstallOpts struct { + cmd.LoggingFlagOpts inputPath string inputCheckSum string - forced bool + force bool } func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { @@ -54,34 +59,36 @@ func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { Long: `Install a Notation plugin Example - Install plugin from file system: - notation plugin install myPlugin.zip + notation plugin install --file myPlugin.tar.gz --checksum 123abcd `, RunE: func(cmd *cobra.Command, args []string) error { return installPlugin(cmd, opts) }, } - command.Flags().StringVar(&opts.inputPath, "file", "", "file path of the plugin to be installed, only supports tar.gz or zip format") + opts.LoggingFlagOpts.ApplyFlags(command.Flags()) + command.Flags().StringVar(&opts.inputPath, "file", "", "file path of the plugin to be installed, only supports tar.gz and zip format") command.Flags().StringVar(&opts.inputCheckSum, "checksum", "", "if set, must match the SHA256 of the plugin tar.gz/zip to be installed") - command.Flags().BoolVar(&opts.forced, "forced", false, "do not force to install and overwrite the plugin") + command.Flags().BoolVar(&opts.force, "force", false, "force to install and overwrite the plugin") command.MarkFlagRequired("file") + command.MarkFlagRequired("checksum") return command } func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { + // set log level + ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context()) inputPath := opts.inputPath // sanity check - iputFileStat, err := os.Stat(inputPath) + inputFileStat, err := os.Stat(inputPath) if err != nil { return fmt.Errorf("failed to install the plugin, %w", err) } - if !iputFileStat.Mode().IsRegular() { + if !inputFileStat.Mode().IsRegular() { return fmt.Errorf("failed to install the plugin, %s is not a regular file", inputPath) } // checkSum check - if opts.inputCheckSum != "" { - if err := validateCheckSum(inputPath, opts.inputCheckSum); err != nil { - return fmt.Errorf("failed to install the plugin, %w", err) - } + if err := validateCheckSum(inputPath, opts.inputCheckSum); err != nil { + return fmt.Errorf("failed to install the plugin, %w", err) } // install the plugin based on file type fileType, err := osutil.DetectFileType(inputPath) @@ -90,11 +97,11 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { } switch fileType { case TypeZip: - if err := installPluginFromZip(command.Context(), inputPath, opts.forced); err != nil { + if err := installPluginFromZip(ctx, inputPath, opts.force); err != nil { return fmt.Errorf("failed to install the plugin, %w", err) } case TypeGzip: - if err := installPluginFromTarGz(command.Context(), inputPath, opts.forced); err != nil { + if err := installPluginFromTarGz(ctx, inputPath, opts.force); err != nil { return fmt.Errorf("failed to install the plugin, %w", err) } default: @@ -114,83 +121,46 @@ func validateCheckSum(path string, checkSum string) error { if err != nil { return err } - if dgst.Encoded() != checkSum { - return errors.New("plugin checkSum does not match user input") + enc := dgst.Encoded() + if enc != checkSum { + return fmt.Errorf("plugin checkSum does not match user input. User input is %s, got %s", checkSum, enc) } return nil } // installPluginFromZip extracts a plugin zip file, validates and // installs the plugin -func installPluginFromZip(ctx context.Context, zipPath string, forced bool) error { +func installPluginFromZip(ctx context.Context, zipPath string, force bool) error { + logger := log.GetLogger(ctx) archive, err := zip.OpenReader(zipPath) if err != nil { return err } defer archive.Close() - tmpDir, err := os.MkdirTemp(".", "unzipTmpDir") - if err != nil { - return err - } - defer os.RemoveAll(tmpDir) for _, f := range archive.File { - fileMode := f.Mode() + fmode := f.Mode() // only consider regular executable files in the zip - if fileMode.IsRegular() && osutil.IsOwnerExecutalbeFile(fileMode) { - pluginName, err := extractPluginNameFromExecutableFileName(f.Name) - // if error is nil, we find the plugin executable file - if err == nil { - // check plugin existence - if !forced { - existed, err := checkPluginExistence(ctx, pluginName) - if err != nil { - return fmt.Errorf("failed to check plugin existence, %w", err) - } - if existed { - return fmt.Errorf("plugin %s already existed", pluginName) - } - } - // extract to tmp dir - tmpFilePath := filepath.Join(tmpDir, filepath.Base(f.Name)) - pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return err - } - fileInArchive, err := f.Open() - if err != nil { - return err - } - defer fileInArchive.Close() - if _, err := io.Copy(pluginFile, fileInArchive); err != nil { - return err - } - if err := pluginFile.Close(); err != nil { - return err - } - // validate plugin metadata - if err := validatePluginMetadata(ctx, pluginName, tmpFilePath); err != nil { - return err - } - // install plugin - pluginPath, err := dir.PluginFS().SysPath(pluginName) - if err != nil { - return nil - } - _, err = osutil.CopyToDir(tmpFilePath, pluginPath) - if err != nil { - return err - } - fmt.Printf("Succussefully installed plugin %s\n", pluginName) - return nil + if fmode.IsRegular() && osutil.IsOwnerExecutalbeFile(fmode) { + fileInArchive, err := f.Open() + if err != nil { + return err } + defer fileInArchive.Close() + err = installPluginExecutable(ctx, f.Name, fileInArchive, fmode, force) + if errors.As(err, ¬ationerrors.ErrorInvalidPluginName{}) { + logger.Warnln(err) + continue + } + return err } } - return errors.New("plugin executable file not found in zip") + return errors.New("valid plugin executable file not found in zip") } // installPluginFromTarGz extracts and untar a plugin tar.gz file, validates and // installs the plugin -func installPluginFromTarGz(ctx context.Context, tarGzPath string, forced bool) error { +func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) error { + logger := log.GetLogger(ctx) r, err := os.Open(tarGzPath) if err != nil { return err @@ -202,11 +172,6 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, forced bool) } defer decompressedStream.Close() tarReader := tar.NewReader(decompressedStream) - tmpDir, err := os.MkdirTemp(".", "untarGzTmpDir") - if err != nil { - return err - } - defer os.RemoveAll(tmpDir) for { header, err := tarReader.Next() if err != nil { @@ -215,62 +180,86 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, forced bool) } return err } - fileMode := header.FileInfo().Mode() + fmode := header.FileInfo().Mode() // only consider regular executable files - if fileMode.IsRegular() && osutil.IsOwnerExecutalbeFile(fileMode) { - pluginName, err := extractPluginNameFromExecutableFileName(header.Name) - // if error is nil, we find the plugin executable file - if err == nil { - // check plugin existence - if !forced { - existed, err := checkPluginExistence(ctx, pluginName) - if err != nil { - return fmt.Errorf("failed to check plugin existence, %w", err) - } - if existed { - return fmt.Errorf("plugin %s already existed", pluginName) - } - } - // extract to tmp dir - tmpFilePath := filepath.Join(tmpDir, header.Name) - pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode()) - if err != nil { - return err - } - if _, err := io.Copy(pluginFile, tarReader); err != nil { - return err - } - if err := pluginFile.Close(); err != nil { - return err - } - // validate plugin metadata - if err := validatePluginMetadata(ctx, pluginName, tmpFilePath); err != nil { - return err - } - // install plugin - pluginPath, err := dir.PluginFS().SysPath(pluginName) - if err != nil { - return nil - } - _, err = osutil.CopyToDir(tmpFilePath, pluginPath) - if err != nil { - return err - } - fmt.Printf("Succussefully installed plugin %s\n", pluginName) - return nil + if fmode.IsRegular() && osutil.IsOwnerExecutalbeFile(fmode) { + err := installPluginExecutable(ctx, header.Name, tarReader, fmode, force) + if errors.As(err, ¬ationerrors.ErrorInvalidPluginName{}) { + logger.Warnln(err) + continue } + return err + } + } + return errors.New("valid plugin executable file not found in tar.gz") +} + +// installPluginExecutable extracts, validates, and installs a plugin from +// reader +func installPluginExecutable(ctx context.Context, fileName string, fileReader io.Reader, fmode fs.FileMode, force bool) error { + pluginName, err := extractPluginNameFromExecutableFileName(fileName) + if err != nil { + return err + } + // check plugin existence + if !force { + existed, err := checkPluginExistence(ctx, pluginName) + if err != nil { + return fmt.Errorf("failed to check plugin existence, %w", err) } + if existed { + return fmt.Errorf("plugin %s already installed", pluginName) + } + } + // extract to tmp dir + tmpDir, err := os.MkdirTemp(".", "pluginTmpDir") + if err != nil { + return fmt.Errorf("failed to create pluginTmpDir, %w", err) } + defer os.RemoveAll(tmpDir) + tmpFilePath := filepath.Join(tmpDir, fileName) + pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fmode) + if err != nil { + return err + } + if _, err := io.Copy(pluginFile, fileReader); err != nil { + return err + } + if err := pluginFile.Close(); err != nil { + return err + } + // validate plugin metadata + pluginVersion, err := validatePluginMetadata(ctx, pluginName, tmpFilePath) + if err != nil { + return err + } + // install plugin + pluginPath, err := dir.PluginFS().SysPath(pluginName) + if err != nil { + return err + } + _, err = osutil.CopyToDir(tmpFilePath, pluginPath) + if err != nil { + return err + } + // plugin is always executable + pluginFilePath := filepath.Join(pluginPath, filepath.Base(tmpFilePath)) + err = os.Chmod(pluginFilePath, 0700) + if err != nil { + return err + } + + fmt.Printf("Succussefully installed plugin %s, version %s\n", pluginName, pluginVersion) return nil } -// extractPluginNameFromExecutableFileName gets plugin name from plugin executable -// file name based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation +// extractPluginNameFromExecutableFileName gets plugin name from plugin +// executable file name based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation func extractPluginNameFromExecutableFileName(execFileName string) (string, error) { fileName := osutil.FileNameWithoutExtension(execFileName) _, pluginName, found := strings.Cut(fileName, "-") if !found || !strings.HasPrefix(fileName, proto.Prefix) { - return "", fmt.Errorf("invalid plugin executable file name. file name requires format notation-{plugin-name}, got %s", fileName) + return "", notationerrors.ErrorInvalidPluginName{Msg: fmt.Sprintf("invalid plugin executable file name. file name requires format notation-{plugin-name}, got %s", fileName)} } return pluginName, nil } @@ -289,14 +278,15 @@ func checkPluginExistence(ctx context.Context, pluginName string) (bool, error) } // validatePluginMetadata validates plugin metadata before installation -func validatePluginMetadata(ctx context.Context, pluginName, path string) error { +// returns the plugin version on success +func validatePluginMetadata(ctx context.Context, pluginName, path string) (string, error) { plugin, err := plugin.NewCLIPlugin(ctx, pluginName, path) if err != nil { - return err + return "", err } - _, err = plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) + metadata, err := plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) if err != nil { - return err + return "", err } - return nil + return metadata.Version, nil } diff --git a/internal/osutil/file.go b/internal/osutil/file.go index c75915e7e..d838ad7ed 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -74,8 +74,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 } @@ -116,6 +116,6 @@ func FileNameWithoutExtension(inputName string) string { } // IsOwnerExecutalbeFile checks whether file is owner executable -func IsOwnerExecutalbeFile(mode fs.FileMode) bool { - return mode&0100 != 0 +func IsOwnerExecutalbeFile(fmode fs.FileMode) bool { + return fmode&0100 != 0 } From bf3d6e9bc12eb251803029a4336c0f4d9ea640c3 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 23 Oct 2023 15:50:47 +0800 Subject: [PATCH 04/83] added uninstall Signed-off-by: Patrick Zheng --- cmd/notation/plugin/cmd.go | 1 + cmd/notation/plugin/install.go | 4 +- cmd/notation/plugin/uninstall.go | 72 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 cmd/notation/plugin/uninstall.go diff --git a/cmd/notation/plugin/cmd.go b/cmd/notation/plugin/cmd.go index 17f3d3150..2500463e5 100644 --- a/cmd/notation/plugin/cmd.go +++ b/cmd/notation/plugin/cmd.go @@ -24,6 +24,7 @@ func Cmd() *cobra.Command { command.AddCommand( pluginListCommand(), pluginInstallCommand(nil), + pluginUninstallCommand(nil), ) return command diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 524072d45..789b6239e 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -59,7 +59,7 @@ func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { Long: `Install a Notation plugin Example - Install plugin from file system: - notation plugin install --file myPlugin.tar.gz --checksum 123abcd + notation plugin install --file myPlugin.tar.gz --checksum abcdef `, RunE: func(cmd *cobra.Command, args []string) error { return installPlugin(cmd, opts) @@ -139,7 +139,7 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error defer archive.Close() for _, f := range archive.File { fmode := f.Mode() - // only consider regular executable files in the zip + // only consider regular executable files if fmode.IsRegular() && osutil.IsOwnerExecutalbeFile(fmode) { fileInArchive, err := f.Open() if err != nil { diff --git a/cmd/notation/plugin/uninstall.go b/cmd/notation/plugin/uninstall.go new file mode 100644 index 000000000..d200f5071 --- /dev/null +++ b/cmd/notation/plugin/uninstall.go @@ -0,0 +1,72 @@ +package plugin + +import ( + "errors" + "fmt" + "os" + + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation/cmd/notation/internal/cmdutil" + "github.com/spf13/cobra" +) + +type pluginUninstallOpts struct { + pluginName string + confirmed bool +} + +func pluginUninstallCommand(opts *pluginUninstallOpts) *cobra.Command { + if opts == nil { + opts = &pluginUninstallOpts{} + } + command := &cobra.Command{ + Use: "uninstall [flags] ", + Short: "Uninstall plugin", + Long: `Uninstall a Notation plugin + +Example - Uninstall plugin: + notation plugin uninstall my-plugin +`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("plugin name is required") + } + opts.pluginName = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return unInstallPlugin(cmd, opts) + }, + } + + command.Flags().BoolVarP(&opts.confirmed, "yes", "y", false, "do not prompt for confirmation") + return command +} + +func unInstallPlugin(command *cobra.Command, opts *pluginUninstallOpts) error { + pluginName := opts.pluginName + existed, err := checkPluginExistence(command.Context(), pluginName) + if err != nil { + return fmt.Errorf("failed to check plugin existence, %w", err) + } + if !existed { + return fmt.Errorf("plugin %s does not exist", pluginName) + } + pluginPath, err := dir.PluginFS().SysPath(pluginName) + if err != nil { + return err + } + 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 err + } + if !confirmed { + return nil + } + err = os.RemoveAll(pluginPath) + if err == nil { + fmt.Printf("Successfully uninstalled plugin %s\n", pluginName) + } + return err +} From a6379c1017d98111fe32882487f476ea56dd0703 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 23 Oct 2023 16:47:20 +0800 Subject: [PATCH 05/83] added install from URL Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 68 +++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 789b6239e..9381cd6e1 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "io/fs" + "net/http" "os" "path/filepath" "strings" @@ -42,9 +43,12 @@ const ( TypeGzip = "application/x-gzip" ) +const notationPluginTmp = "notationPluginTmp" + type pluginInstallOpts struct { cmd.LoggingFlagOpts inputPath string + inputURL string inputCheckSum string force bool } @@ -54,13 +58,28 @@ func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { opts = &pluginInstallOpts{} } command := &cobra.Command{ - Use: "install [flags] ", + Use: "install [flags] ", Short: "Install plugin", Long: `Install a Notation plugin Example - Install plugin from file system: - notation plugin install --file myPlugin.tar.gz --checksum abcdef + notation plugin install --file myPlugin.zip --checksum abcdef + +Example - Install plugin from URL: + notation plugin install https://wabbit-networks.com/intaller/linux/amd64/wabbit-plugin-v1.0.tar.gz --checksum abcxyz `, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 && opts.inputPath == "" { + return errors.New("missing plugin URL or file path") + } + if len(args) != 0 && opts.inputPath != "" { + return errors.New("can install from either plugin URL or file path, got both") + } + if len(args) != 0 { + opts.inputURL = args[0] + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { return installPlugin(cmd, opts) }, @@ -69,7 +88,6 @@ Example - Install plugin from file system: command.Flags().StringVar(&opts.inputPath, "file", "", "file path of the plugin to be installed, only supports tar.gz and zip format") command.Flags().StringVar(&opts.inputCheckSum, "checksum", "", "if set, must match the SHA256 of the plugin tar.gz/zip to be installed") command.Flags().BoolVar(&opts.force, "force", false, "force to install and overwrite the plugin") - command.MarkFlagRequired("file") command.MarkFlagRequired("checksum") return command } @@ -77,7 +95,17 @@ Example - Install plugin from file system: func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { // set log level ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context()) + inputPath := opts.inputPath + // install from URL + if opts.inputURL != "" { + inputPath = notationPluginTmp + if err := downloadFromURL(ctx, inputPath, opts.inputURL); err != nil { + return err + } + defer os.Remove(inputPath) + } + // sanity check inputFileStat, err := os.Stat(inputPath) if err != nil { @@ -108,6 +136,7 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { return errors.New("failed to install the plugin, invalid file type. Only support tar.gz and zip") } return nil + } // validateCheckSum returns nil if SHA256 of file at path equals to checkSum. @@ -212,9 +241,9 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io } } // extract to tmp dir - tmpDir, err := os.MkdirTemp(".", "pluginTmpDir") + tmpDir, err := os.MkdirTemp(".", notationPluginTmp) if err != nil { - return fmt.Errorf("failed to create pluginTmpDir, %w", err) + return fmt.Errorf("failed to create notationPluginTmp, %w", err) } defer os.RemoveAll(tmpDir) tmpFilePath := filepath.Join(tmpDir, fileName) @@ -290,3 +319,32 @@ func validatePluginMetadata(ctx context.Context, pluginName, path string) (strin } return metadata.Version, nil } + +func downloadFromURL(ctx context.Context, filePath, url string) error { + // Create the file + out, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer out.Close() + + // Get the data + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + // Check server response + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + // Writer the body to file + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + + return nil +} From 4b5989cde14b62f21152004a941935a4db1f1375 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 24 Oct 2023 14:06:25 +0800 Subject: [PATCH 06/83] updated Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 35 +++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 9381cd6e1..54fafc44f 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -43,7 +43,7 @@ const ( TypeGzip = "application/x-gzip" ) -const notationPluginTmp = "notationPluginTmp" +const notationPluginTmpDir = "notationPluginTmpDir" type pluginInstallOpts struct { cmd.LoggingFlagOpts @@ -99,11 +99,15 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { inputPath := opts.inputPath // install from URL if opts.inputURL != "" { - inputPath = notationPluginTmp - if err := downloadFromURL(ctx, inputPath, opts.inputURL); err != nil { + tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) + if err != nil { + return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) + } + defer os.RemoveAll(tmpDir) + inputPath, err = downloadPluginFromURL(ctx, opts.inputURL, tmpDir) + if err != nil { return err } - defer os.Remove(inputPath) } // sanity check @@ -183,7 +187,7 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error return err } } - return errors.New("valid plugin executable file not found in zip") + return errors.New("no valid plugin executable file was found in zip") } // installPluginFromTarGz extracts and untar a plugin tar.gz file, validates and @@ -220,7 +224,7 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e return err } } - return errors.New("valid plugin executable file not found in tar.gz") + return errors.New("no valid plugin executable file was found in tar.gz") } // installPluginExecutable extracts, validates, and installs a plugin from @@ -241,9 +245,9 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io } } // extract to tmp dir - tmpDir, err := os.MkdirTemp(".", notationPluginTmp) + tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) if err != nil { - return fmt.Errorf("failed to create notationPluginTmp, %w", err) + return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) } defer os.RemoveAll(tmpDir) tmpFilePath := filepath.Join(tmpDir, fileName) @@ -320,31 +324,32 @@ func validatePluginMetadata(ctx context.Context, pluginName, path string) (strin return metadata.Version, nil } -func downloadFromURL(ctx context.Context, filePath, url string) error { +func downloadPluginFromURL(ctx context.Context, url, dir string) (string, error) { // Create the file - out, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + tmpFilePath := filepath.Join(dir, "notationPluginTmp") + out, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { - return err + return "", err } defer out.Close() // Get the data resp, err := http.Get(url) if err != nil { - return err + return "", err } defer resp.Body.Close() // Check server response if resp.StatusCode != http.StatusOK { - return fmt.Errorf("bad status: %s", resp.Status) + return "", fmt.Errorf("bad status: %s", resp.Status) } // Writer the body to file _, err = io.Copy(out, resp.Body) if err != nil { - return err + return "", err } - return nil + return tmpFilePath, err } From 155023675ab17d01a8700a95dc3bdd3227e6f32b Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 24 Oct 2023 16:27:16 +0800 Subject: [PATCH 07/83] fix Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 54fafc44f..31645ef2d 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -25,6 +25,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" "strings" "github.com/notaryproject/notation-go/dir" @@ -230,10 +231,18 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e // installPluginExecutable extracts, validates, and installs a plugin from // reader func installPluginExecutable(ctx context.Context, fileName string, fileReader io.Reader, fmode fs.FileMode, force bool) error { + // sanity check pluginName, err := extractPluginNameFromExecutableFileName(fileName) if err != nil { return err } + if runtime.GOOS == "windows" && filepath.Ext(fileName) != ".exe" { + return fmt.Errorf("on Windows, plugin executable file name %s is missing the '.exe' extension", fileName) + } + if runtime.GOOS != "windows" && filepath.Ext(fileName) == ".exe" { + return fmt.Errorf("on %s, plugin executable file name %s cannot have the '.exe' extension", runtime.GOOS, fileName) + } + // check plugin existence if !force { existed, err := checkPluginExistence(ctx, pluginName) From ea75514a00d042363b31fcaee5cddf5a631d7007 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 24 Oct 2023 17:01:28 +0800 Subject: [PATCH 08/83] updated Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 31645ef2d..9ebc56205 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -110,7 +110,6 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { return err } } - // sanity check inputFileStat, err := os.Stat(inputPath) if err != nil { @@ -141,7 +140,6 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { return errors.New("failed to install the plugin, invalid file type. Only support tar.gz and zip") } return nil - } // validateCheckSum returns nil if SHA256 of file at path equals to checkSum. @@ -173,7 +171,8 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error defer archive.Close() for _, f := range archive.File { fmode := f.Mode() - // only consider regular executable files + // requires one and only one executable file, with name in format + // notation-{plugin-name}, exists in the zip file if fmode.IsRegular() && osutil.IsOwnerExecutalbeFile(fmode) { fileInArchive, err := f.Open() if err != nil { @@ -188,7 +187,7 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error return err } } - return errors.New("no valid plugin executable file was found in zip") + return fmt.Errorf("no valid plugin executable file was found in %s", zipPath) } // installPluginFromTarGz extracts and untar a plugin tar.gz file, validates and @@ -215,7 +214,8 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e return err } fmode := header.FileInfo().Mode() - // only consider regular executable files + // requires one and only one executable file, with name in format + // notation-{plugin-name}, exists in the tar.gz file if fmode.IsRegular() && osutil.IsOwnerExecutalbeFile(fmode) { err := installPluginExecutable(ctx, header.Name, tarReader, fmode, force) if errors.As(err, ¬ationerrors.ErrorInvalidPluginName{}) { @@ -225,7 +225,7 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e return err } } - return errors.New("no valid plugin executable file was found in tar.gz") + return fmt.Errorf("no valid plugin executable file was found in %s", tarGzPath) } // installPluginExecutable extracts, validates, and installs a plugin from @@ -285,7 +285,7 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io return err } // plugin is always executable - pluginFilePath := filepath.Join(pluginPath, filepath.Base(tmpFilePath)) + pluginFilePath := filepath.Join(pluginPath, fileName) err = os.Chmod(pluginFilePath, 0700) if err != nil { return err @@ -319,7 +319,7 @@ func checkPluginExistence(ctx context.Context, pluginName string) (bool, error) return true, nil } -// validatePluginMetadata validates plugin metadata before installation +// validatePluginMetadata validates plugin metadata given plugin name and path // returns the plugin version on success func validatePluginMetadata(ctx context.Context, pluginName, path string) (string, error) { plugin, err := plugin.NewCLIPlugin(ctx, pluginName, path) @@ -333,32 +333,30 @@ func validatePluginMetadata(ctx context.Context, pluginName, path string) (strin return metadata.Version, nil } -func downloadPluginFromURL(ctx context.Context, url, dir string) (string, error) { +// downloadPluginFromURL downloads plugin file from url to a tmp directory +// it returns the tmp file path of the downloaded file +func downloadPluginFromURL(ctx context.Context, url, tmpDir string) (string, error) { // Create the file - tmpFilePath := filepath.Join(dir, "notationPluginTmp") + tmpFilePath := filepath.Join(tmpDir, "notationPluginTmp") out, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return "", err } defer out.Close() - // Get the data resp, err := http.Get(url) if err != nil { return "", err } defer resp.Body.Close() - // Check server response if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("bad status: %s", resp.Status) } - // Writer the body to file _, err = io.Copy(out, resp.Body) if err != nil { return "", err } - return tmpFilePath, err } From 236a8aa151d79d5e3466698cc1db7479ac68e1d2 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 31 Oct 2023 16:46:15 +0800 Subject: [PATCH 09/83] notation plugin install Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 23 +++++++ cmd/notation/internal/plugin/plugin_test.go | 13 ++++ cmd/notation/plugin/cmd.go | 1 - cmd/notation/plugin/uninstall.go | 72 --------------------- 4 files changed, 36 insertions(+), 73 deletions(-) create mode 100644 cmd/notation/internal/plugin/plugin.go create mode 100644 cmd/notation/internal/plugin/plugin_test.go delete mode 100644 cmd/notation/plugin/uninstall.go diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go new file mode 100644 index 000000000..dce8ce70b --- /dev/null +++ b/cmd/notation/internal/plugin/plugin.go @@ -0,0 +1,23 @@ +package plugin + +import ( + "context" + "errors" + "os" + + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/plugin" +) + +// CheckPluginExistence returns true if a plugin already exists +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) { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go new file mode 100644 index 000000000..85d04fdd5 --- /dev/null +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -0,0 +1,13 @@ +package plugin + +import ( + "context" + "testing" +) + +func TestCheckPluginExistence(t *testing.T) { + exist, err := CheckPluginExistence(context.Background(), "non-exist-plugin") + if exist || err != nil { + t.Fatalf("expected exist to be false with nil err, got: %v, %s", exist, err) + } +} diff --git a/cmd/notation/plugin/cmd.go b/cmd/notation/plugin/cmd.go index 2500463e5..17f3d3150 100644 --- a/cmd/notation/plugin/cmd.go +++ b/cmd/notation/plugin/cmd.go @@ -24,7 +24,6 @@ func Cmd() *cobra.Command { command.AddCommand( pluginListCommand(), pluginInstallCommand(nil), - pluginUninstallCommand(nil), ) return command diff --git a/cmd/notation/plugin/uninstall.go b/cmd/notation/plugin/uninstall.go deleted file mode 100644 index d200f5071..000000000 --- a/cmd/notation/plugin/uninstall.go +++ /dev/null @@ -1,72 +0,0 @@ -package plugin - -import ( - "errors" - "fmt" - "os" - - "github.com/notaryproject/notation-go/dir" - "github.com/notaryproject/notation/cmd/notation/internal/cmdutil" - "github.com/spf13/cobra" -) - -type pluginUninstallOpts struct { - pluginName string - confirmed bool -} - -func pluginUninstallCommand(opts *pluginUninstallOpts) *cobra.Command { - if opts == nil { - opts = &pluginUninstallOpts{} - } - command := &cobra.Command{ - Use: "uninstall [flags] ", - Short: "Uninstall plugin", - Long: `Uninstall a Notation plugin - -Example - Uninstall plugin: - notation plugin uninstall my-plugin -`, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.New("plugin name is required") - } - opts.pluginName = args[0] - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - return unInstallPlugin(cmd, opts) - }, - } - - command.Flags().BoolVarP(&opts.confirmed, "yes", "y", false, "do not prompt for confirmation") - return command -} - -func unInstallPlugin(command *cobra.Command, opts *pluginUninstallOpts) error { - pluginName := opts.pluginName - existed, err := checkPluginExistence(command.Context(), pluginName) - if err != nil { - return fmt.Errorf("failed to check plugin existence, %w", err) - } - if !existed { - return fmt.Errorf("plugin %s does not exist", pluginName) - } - pluginPath, err := dir.PluginFS().SysPath(pluginName) - if err != nil { - return err - } - 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 err - } - if !confirmed { - return nil - } - err = os.RemoveAll(pluginPath) - if err == nil { - fmt.Printf("Successfully uninstalled plugin %s\n", pluginName) - } - return err -} From 3c6ca5fb6530cf820af3f8d87ee9424f7d8086ee Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 2 Nov 2023 10:03:39 +0800 Subject: [PATCH 10/83] update Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 74 +++++++++++++++++++++ cmd/notation/internal/plugin/plugin_test.go | 24 +++++++ cmd/notation/internal/plugin/testdata/test | 0 cmd/notation/plugin/install.go | 69 ++----------------- 4 files changed, 103 insertions(+), 64 deletions(-) create mode 100644 cmd/notation/internal/plugin/testdata/test diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index dce8ce70b..ba87d245c 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -3,10 +3,31 @@ package plugin import ( "context" "errors" + "fmt" "os" + "strings" "github.com/notaryproject/notation-go/dir" "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/plugin/proto" + notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" + "github.com/notaryproject/notation/internal/osutil" + "github.com/opencontainers/go-digest" +) + +// Type is an enum for plugin source types. +type Type string + +const ( + TypeFile Type = "file" + TypeURL Type = "url" +) + +var ( + Types = []Type{ + TypeFile, + TypeURL, + } ) // CheckPluginExistence returns true if a plugin already exists @@ -21,3 +42,56 @@ func CheckPluginExistence(ctx context.Context, pluginName string) (bool, error) } return true, nil } + +// ValidateInstallSource returns true if source is in Types +func ValidateInstallSource(source string) bool { + for _, t := range Types { + if strings.ToLower(source) == string(t) { + return true + } + } + return false +} + +// ValidateCheckSum returns nil if SHA256 of file at path equals to checkSum. +func ValidateCheckSum(path string, checkSum string) error { + r, err := os.Open(path) + if err != nil { + return err + } + defer r.Close() + dgst, err := digest.FromReader(r) + if err != nil { + return err + } + enc := dgst.Encoded() + if enc != checkSum { + return fmt.Errorf("plugin checkSum does not match user input. User input is %s, got %s", checkSum, enc) + } + return nil +} + +// ValidatePluginMetadata validates plugin metadata given plugin name and path +// returns the plugin version on success +func ValidatePluginMetadata(ctx context.Context, pluginName, path string) (string, error) { + plugin, err := plugin.NewCLIPlugin(ctx, pluginName, path) + if err != nil { + return "", err + } + metadata, err := plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) + if err != nil { + return "", err + } + return metadata.Version, nil +} + +// ExtractPluginNameFromExecutableFileName gets plugin name from plugin +// executable file name based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation +func ExtractPluginNameFromExecutableFileName(execFileName string) (string, error) { + fileName := osutil.FileNameWithoutExtension(execFileName) + _, pluginName, found := strings.Cut(fileName, "-") + if !found || !strings.HasPrefix(fileName, proto.Prefix) { + return "", notationerrors.ErrorInvalidPluginName{Msg: fmt.Sprintf("invalid plugin executable file name. file name requires format notation-{plugin-name}, got %s", fileName)} + } + return pluginName, nil +} diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go index 85d04fdd5..a3a8d7fac 100644 --- a/cmd/notation/internal/plugin/plugin_test.go +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -11,3 +11,27 @@ func TestCheckPluginExistence(t *testing.T) { t.Fatalf("expected exist to be false with nil err, got: %v, %s", exist, err) } } + +func TestValidateInstallSource(t *testing.T) { + if !ValidateInstallSource("file") || !ValidateInstallSource("url") { + t.Fatalf("file and url should be supported") + } + + if ValidateInstallSource("invalid") { + t.Fatalf("invalid should be unsupported") + } +} + +func TestValidateCheckSum(t *testing.T) { + expectedErrorMsg := "plugin checkSum does not match user input. User input is invalid, got e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + if err := ValidateCheckSum("./testdata/test", "invalid"); err == nil || err.Error() != expectedErrorMsg { + t.Fatalf("expected err %s, got %v", expectedErrorMsg, err) + } + if err := ValidateCheckSum("./testdata/test", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); err != nil { + t.Fatalf("expected nil err, got %v", err) + } +} + +func TestExtractPluginNameFromExecutableFileName(t *testing.T) { + +} diff --git a/cmd/notation/internal/plugin/testdata/test b/cmd/notation/internal/plugin/testdata/test new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 9ebc56205..e24f2bda1 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -26,16 +26,13 @@ import ( "os" "path/filepath" "runtime" - "strings" "github.com/notaryproject/notation-go/dir" "github.com/notaryproject/notation-go/log" - "github.com/notaryproject/notation-go/plugin" - "github.com/notaryproject/notation-go/plugin/proto" notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" + notationplugin "github.com/notaryproject/notation/cmd/notation/internal/plugin" "github.com/notaryproject/notation/internal/cmd" "github.com/notaryproject/notation/internal/osutil" - "github.com/opencontainers/go-digest" "github.com/spf13/cobra" ) @@ -119,7 +116,7 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { return fmt.Errorf("failed to install the plugin, %s is not a regular file", inputPath) } // checkSum check - if err := validateCheckSum(inputPath, opts.inputCheckSum); err != nil { + if err := notationplugin.ValidateCheckSum(inputPath, opts.inputCheckSum); err != nil { return fmt.Errorf("failed to install the plugin, %w", err) } // install the plugin based on file type @@ -142,24 +139,6 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { return nil } -// validateCheckSum returns nil if SHA256 of file at path equals to checkSum. -func validateCheckSum(path string, checkSum string) error { - r, err := os.Open(path) - if err != nil { - return err - } - defer r.Close() - dgst, err := digest.FromReader(r) - if err != nil { - return err - } - enc := dgst.Encoded() - if enc != checkSum { - return fmt.Errorf("plugin checkSum does not match user input. User input is %s, got %s", checkSum, enc) - } - return nil -} - // installPluginFromZip extracts a plugin zip file, validates and // installs the plugin func installPluginFromZip(ctx context.Context, zipPath string, force bool) error { @@ -232,7 +211,7 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e // reader func installPluginExecutable(ctx context.Context, fileName string, fileReader io.Reader, fmode fs.FileMode, force bool) error { // sanity check - pluginName, err := extractPluginNameFromExecutableFileName(fileName) + pluginName, err := notationplugin.ExtractPluginNameFromExecutableFileName(fileName) if err != nil { return err } @@ -245,7 +224,7 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io // check plugin existence if !force { - existed, err := checkPluginExistence(ctx, pluginName) + existed, err := notationplugin.CheckPluginExistence(ctx, pluginName) if err != nil { return fmt.Errorf("failed to check plugin existence, %w", err) } @@ -271,7 +250,7 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io return err } // validate plugin metadata - pluginVersion, err := validatePluginMetadata(ctx, pluginName, tmpFilePath) + pluginVersion, err := notationplugin.ValidatePluginMetadata(ctx, pluginName, tmpFilePath) if err != nil { return err } @@ -295,44 +274,6 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io return nil } -// extractPluginNameFromExecutableFileName gets plugin name from plugin -// executable file name based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation -func extractPluginNameFromExecutableFileName(execFileName string) (string, error) { - fileName := osutil.FileNameWithoutExtension(execFileName) - _, pluginName, found := strings.Cut(fileName, "-") - if !found || !strings.HasPrefix(fileName, proto.Prefix) { - return "", notationerrors.ErrorInvalidPluginName{Msg: fmt.Sprintf("invalid plugin executable file name. file name requires format notation-{plugin-name}, got %s", fileName)} - } - return pluginName, nil -} - -// checkPluginExistence returns true if a plugin already exists -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) { - return false, nil - } - return false, err - } - return true, nil -} - -// validatePluginMetadata validates plugin metadata given plugin name and path -// returns the plugin version on success -func validatePluginMetadata(ctx context.Context, pluginName, path string) (string, error) { - plugin, err := plugin.NewCLIPlugin(ctx, pluginName, path) - if err != nil { - return "", err - } - metadata, err := plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) - if err != nil { - return "", err - } - return metadata.Version, nil -} - // downloadPluginFromURL downloads plugin file from url to a tmp directory // it returns the tmp file path of the downloaded file func downloadPluginFromURL(ctx context.Context, url, tmpDir string) (string, error) { From 121b783797f75b11b126fc9db805887620a028b2 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 3 Nov 2023 08:37:58 +0800 Subject: [PATCH 11/83] updated Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 31 +++++++++++++++++++++ cmd/notation/internal/plugin/plugin_test.go | 26 +++++++++++++++++ cmd/notation/plugin/install.go | 31 +-------------------- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index ba87d245c..b496e42f0 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -4,7 +4,10 @@ import ( "context" "errors" "fmt" + "io" + "net/http" "os" + "path/filepath" "strings" "github.com/notaryproject/notation-go/dir" @@ -85,6 +88,34 @@ func ValidatePluginMetadata(ctx context.Context, pluginName, path string) (strin return metadata.Version, nil } +// DownloadPluginFromURL downloads plugin file from url to a tmp directory +// it returns the tmp file path of the downloaded file +func DownloadPluginFromURL(ctx context.Context, url, tmpDir string) (string, error) { + // Create the file + tmpFilePath := filepath.Join(tmpDir, "notationPluginTmp") + out, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return "", err + } + defer out.Close() + // Get the data + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + // Check server response + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("bad status: %s", resp.Status) + } + // Writer the body to file + _, err = io.Copy(out, resp.Body) + if err != nil { + return "", err + } + return tmpFilePath, err +} + // ExtractPluginNameFromExecutableFileName gets plugin name from plugin // executable file name based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation func ExtractPluginNameFromExecutableFileName(execFileName string) (string, error) { diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go index a3a8d7fac..8d8eab5f0 100644 --- a/cmd/notation/internal/plugin/plugin_test.go +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -33,5 +33,31 @@ func TestValidateCheckSum(t *testing.T) { } func TestExtractPluginNameFromExecutableFileName(t *testing.T) { + pluginName, err := ExtractPluginNameFromExecutableFileName("notation-my-plugin") + if err != nil { + t.Fatalf("expected nil err, got %v", err) + } + if pluginName != "my-pluing" { + t.Fatalf("expected plugin name my-plugin, got %s", pluginName) + } + + pluginName, err = ExtractPluginNameFromExecutableFileName("notation-my-plugin.exe") + if err != nil { + t.Fatalf("expected nil err, got %v", err) + } + if pluginName != "my-pluing" { + t.Fatalf("expected plugin name my-plugin, got %s", pluginName) + } + _, err = ExtractPluginNameFromExecutableFileName("myPlugin") + expectedErrorMsg := "invalid plugin executable file name. file name requires format notation-{plugin-name}, got myPlugin" + if err == nil || err.Error() != expectedErrorMsg { + t.Fatalf("expected %s, got %v", expectedErrorMsg, err) + } + + _, err = ExtractPluginNameFromExecutableFileName("my-plugin") + expectedErrorMsg = "invalid plugin executable file name. file name requires format notation-{plugin-name}, got my-plugin" + if err == nil || err.Error() != expectedErrorMsg { + t.Fatalf("expected %s, got %v", expectedErrorMsg, err) + } } diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index e24f2bda1..7e892141f 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -22,7 +22,6 @@ import ( "fmt" "io" "io/fs" - "net/http" "os" "path/filepath" "runtime" @@ -102,7 +101,7 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) } defer os.RemoveAll(tmpDir) - inputPath, err = downloadPluginFromURL(ctx, opts.inputURL, tmpDir) + inputPath, err = notationplugin.DownloadPluginFromURL(ctx, opts.inputURL, tmpDir) if err != nil { return err } @@ -273,31 +272,3 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io fmt.Printf("Succussefully installed plugin %s, version %s\n", pluginName, pluginVersion) return nil } - -// downloadPluginFromURL downloads plugin file from url to a tmp directory -// it returns the tmp file path of the downloaded file -func downloadPluginFromURL(ctx context.Context, url, tmpDir string) (string, error) { - // Create the file - tmpFilePath := filepath.Join(tmpDir, "notationPluginTmp") - out, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return "", err - } - defer out.Close() - // Get the data - resp, err := http.Get(url) - if err != nil { - return "", err - } - defer resp.Body.Close() - // Check server response - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("bad status: %s", resp.Status) - } - // Writer the body to file - _, err = io.Copy(out, resp.Body) - if err != nil { - return "", err - } - return tmpFilePath, err -} From 0743cb0b56dadb2135b176d28ad8152f10099b31 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 3 Nov 2023 11:48:38 +0800 Subject: [PATCH 12/83] updated unit tests Signed-off-by: Patrick Zheng --- internal/osutil/file_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/osutil/file_test.go b/internal/osutil/file_test.go index 6bd70e541..abfe76ca0 100644 --- a/internal/osutil/file_test.go +++ b/internal/osutil/file_test.go @@ -15,6 +15,7 @@ package osutil import ( "bytes" + "io/fs" "io/ioutil" "os" "path/filepath" @@ -260,3 +261,21 @@ func TestCopyToDir(t *testing.T) { validFileContent(t, filepath.Join(destDir, "file.txt"), data) }) } + +func TestFileNameWithoutExtension(t *testing.T) { + input := "testfile.tar.gz" + expectedOutput := "testfile.tar" + actualOutput := FileNameWithoutExtension(input) + if actualOutput != expectedOutput { + t.Errorf("expected '%s', but got '%s'", expectedOutput, actualOutput) + } +} + +func TestIsOwnerExecutalbeFile(t *testing.T) { + input := fs.FileMode(0755) + expectedOutput := true + actualOutput := IsOwnerExecutalbeFile(input) + if actualOutput != expectedOutput { + t.Errorf("expected '%t', but got '%t'", expectedOutput, actualOutput) + } +} From 932e89a3e6a58dd9db112c75e4caffe0495660ac Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 7 Nov 2023 16:12:37 +0800 Subject: [PATCH 13/83] updated based on spec changes Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 41 ++++--- cmd/notation/internal/plugin/plugin_test.go | 10 -- cmd/notation/plugin/install.go | 117 +++++++++++++------- 3 files changed, 96 insertions(+), 72 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index b496e42f0..e074276f9 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -18,19 +18,26 @@ import ( "github.com/opencontainers/go-digest" ) -// Type is an enum for plugin source types. -type Type string +// PluginSourceType is an enum for plugin source +type PluginSourceType string const ( - TypeFile Type = "file" - TypeURL Type = "url" + // TypeFile means plugin source is file + TypeFile PluginSourceType = "file" + + // TypeURL means plugin source is URL + TypeURL PluginSourceType = "url" + + // TypeUnknown means unknown plugin source + TypeUnknown PluginSourceType = "unknown" ) -var ( - Types = []Type{ - TypeFile, - TypeURL, - } +const ( + // TypeZip means plugin file is zip + TypeZip = "application/zip" + + // TypeGzip means plugin file is gzip + TypeGzip = "application/x-gzip" ) // CheckPluginExistence returns true if a plugin already exists @@ -46,16 +53,6 @@ func CheckPluginExistence(ctx context.Context, pluginName string) (bool, error) return true, nil } -// ValidateInstallSource returns true if source is in Types -func ValidateInstallSource(source string) bool { - for _, t := range Types { - if strings.ToLower(source) == string(t) { - return true - } - } - return false -} - // ValidateCheckSum returns nil if SHA256 of file at path equals to checkSum. func ValidateCheckSum(path string, checkSum string) error { r, err := os.Open(path) @@ -69,12 +66,12 @@ func ValidateCheckSum(path string, checkSum string) error { } enc := dgst.Encoded() if enc != checkSum { - return fmt.Errorf("plugin checkSum does not match user input. User input is %s, got %s", checkSum, enc) + return fmt.Errorf("plugin checksum does not match user input. Expecting %s", checkSum) } return nil } -// ValidatePluginMetadata validates plugin metadata given plugin name and path +// ValidatePluginMetadata validates plugin metadata given plugin name and path, // returns the plugin version on success func ValidatePluginMetadata(ctx context.Context, pluginName, path string) (string, error) { plugin, err := plugin.NewCLIPlugin(ctx, pluginName, path) @@ -122,7 +119,7 @@ func ExtractPluginNameFromExecutableFileName(execFileName string) (string, error fileName := osutil.FileNameWithoutExtension(execFileName) _, pluginName, found := strings.Cut(fileName, "-") if !found || !strings.HasPrefix(fileName, proto.Prefix) { - return "", notationerrors.ErrorInvalidPluginName{Msg: fmt.Sprintf("invalid plugin executable file name. file name requires format notation-{plugin-name}, got %s", fileName)} + return "", notationerrors.ErrorInvalidPluginName{Msg: fmt.Sprintf("invalid plugin executable file name. file name requires format notation-{plugin-name}, but got %s", fileName)} } return pluginName, nil } diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go index 8d8eab5f0..8295aa8aa 100644 --- a/cmd/notation/internal/plugin/plugin_test.go +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -12,16 +12,6 @@ func TestCheckPluginExistence(t *testing.T) { } } -func TestValidateInstallSource(t *testing.T) { - if !ValidateInstallSource("file") || !ValidateInstallSource("url") { - t.Fatalf("file and url should be supported") - } - - if ValidateInstallSource("invalid") { - t.Fatalf("invalid should be unsupported") - } -} - func TestValidateCheckSum(t *testing.T) { expectedErrorMsg := "plugin checkSum does not match user input. User input is invalid, got e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" if err := ValidateCheckSum("./testdata/test", "invalid"); err == nil || err.Error() != expectedErrorMsg { diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 7e892141f..364e8fd65 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -22,9 +22,11 @@ import ( "fmt" "io" "io/fs" + "net/url" "os" "path/filepath" "runtime" + "strings" "github.com/notaryproject/notation-go/dir" "github.com/notaryproject/notation-go/log" @@ -35,17 +37,11 @@ import ( "github.com/spf13/cobra" ) -const ( - TypeZip = "application/zip" - TypeGzip = "application/x-gzip" -) - const notationPluginTmpDir = "notationPluginTmpDir" type pluginInstallOpts struct { cmd.LoggingFlagOpts - inputPath string - inputURL string + pluginSource string inputCheckSum string force bool } @@ -60,21 +56,25 @@ func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { Long: `Install a Notation plugin Example - Install plugin from file system: - notation plugin install --file myPlugin.zip --checksum abcdef + notation plugin install wabbit-plugin-v1.0.zip + +Example - Install plugin from file system with user input SHA256 checksum: + notation plugin install wabbit-plugin-v1.0.zip --checksum abcdef + +Example - Install plugin from file system regardless if it's already installed: + notation plugin install wabbit-plugin-v1.0.zip --force -Example - Install plugin from URL: - notation plugin install https://wabbit-networks.com/intaller/linux/amd64/wabbit-plugin-v1.0.tar.gz --checksum abcxyz +Example - Install plugin from file system with .tar.gz: + notation plugin install wabbit-plugin-v1.0.tar.gz + +Example - Install plugin from URL, SHA256 checksum is required: + notation plugin install --checksum abcxyz https://wabbit-networks.com/intaller/linux/amd64/wabbit-plugin-v1.0.tar.gz `, Args: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 && opts.inputPath == "" { - return errors.New("missing plugin URL or file path") - } - if len(args) != 0 && opts.inputPath != "" { - return errors.New("can install from either plugin URL or file path, got both") - } - if len(args) != 0 { - opts.inputURL = args[0] + if len(args) == 0 { + return errors.New("missing plugin file path or URL") } + opts.pluginSource = args[0] return nil }, RunE: func(cmd *cobra.Command, args []string) error { @@ -82,30 +82,44 @@ Example - Install plugin from URL: }, } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) - command.Flags().StringVar(&opts.inputPath, "file", "", "file path of the plugin to be installed, only supports tar.gz and zip format") - command.Flags().StringVar(&opts.inputCheckSum, "checksum", "", "if set, must match the SHA256 of the plugin tar.gz/zip to be installed") + command.Flags().StringVar(&opts.inputCheckSum, "checksum", "", "if set, must match the SHA256 of the plugin source") command.Flags().BoolVar(&opts.force, "force", false, "force to install and overwrite the plugin") - command.MarkFlagRequired("checksum") return command } func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { // set log level ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context()) - - inputPath := opts.inputPath - // install from URL - if opts.inputURL != "" { - tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) + // get plugin source type, support file and URL + pluginSourceType, err := getPluginSource(opts.pluginSource) + if err != nil { + return err + } + // core process + switch pluginSourceType { + case notationplugin.TypeFile: + return installFromFileSystem(ctx, opts.pluginSource, opts.inputCheckSum, opts.force) + case notationplugin.TypeURL: + if opts.inputCheckSum == "" { + return errors.New("install from URL requires non-empty SHA256 checksum of the plugin source") + } + tmpDir, err := os.MkdirTemp(".", notationPluginTmpDir) if err != nil { return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) } defer os.RemoveAll(tmpDir) - inputPath, err = notationplugin.DownloadPluginFromURL(ctx, opts.inputURL, tmpDir) + downloadPath, err := notationplugin.DownloadPluginFromURL(ctx, opts.pluginSource, tmpDir) if err != nil { - return err + return fmt.Errorf("failed to download plugin from URL %s with error: %w", opts.pluginSource, err) } + return installFromFileSystem(ctx, downloadPath, opts.inputCheckSum, opts.force) + default: + return fmt.Errorf("failed to install the plugin, plugin source type %s is not supported", pluginSourceType) } +} + +// installFromFileSystem install the plugin from file system +func installFromFileSystem(ctx context.Context, inputPath string, inputCheckSum string, force bool) error { // sanity check inputFileStat, err := os.Stat(inputPath) if err != nil { @@ -114,9 +128,10 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { if !inputFileStat.Mode().IsRegular() { return fmt.Errorf("failed to install the plugin, %s is not a regular file", inputPath) } - // checkSum check - if err := notationplugin.ValidateCheckSum(inputPath, opts.inputCheckSum); err != nil { - return fmt.Errorf("failed to install the plugin, %w", err) + if inputCheckSum != "" { + if err := notationplugin.ValidateCheckSum(inputPath, inputCheckSum); err != nil { + return fmt.Errorf("failed to install the plugin, %w", err) + } } // install the plugin based on file type fileType, err := osutil.DetectFileType(inputPath) @@ -124,18 +139,20 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { return fmt.Errorf("failed to install the plugin, %w", err) } switch fileType { - case TypeZip: - if err := installPluginFromZip(ctx, inputPath, opts.force); err != nil { + case notationplugin.TypeZip: + if err := installPluginFromZip(ctx, inputPath, force); err != nil { return fmt.Errorf("failed to install the plugin, %w", err) } - case TypeGzip: - if err := installPluginFromTarGz(ctx, inputPath, opts.force); err != nil { + return nil + case notationplugin.TypeGzip: + // when file is gzip, require to be tar + if err := installPluginFromTarGz(ctx, inputPath, force); err != nil { return fmt.Errorf("failed to install the plugin, %w", err) } + return nil default: - return errors.New("failed to install the plugin, invalid file type. Only support tar.gz and zip") + return errors.New("failed to install the plugin, invalid file format. Only support .tar.gz and .zip") } - return nil } // installPluginFromZip extracts a plugin zip file, validates and @@ -220,7 +237,6 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io if runtime.GOOS != "windows" && filepath.Ext(fileName) == ".exe" { return fmt.Errorf("on %s, plugin executable file name %s cannot have the '.exe' extension", runtime.GOOS, fileName) } - // check plugin existence if !force { existed, err := notationplugin.CheckPluginExistence(ctx, pluginName) @@ -232,7 +248,7 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io } } // extract to tmp dir - tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) + tmpDir, err := os.MkdirTemp(".", notationPluginTmpDir) if err != nil { return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) } @@ -268,7 +284,28 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io if err != nil { return err } - fmt.Printf("Succussefully installed plugin %s, version %s\n", pluginName, pluginVersion) return nil } + +// getPluginSource returns the type of plugin source +func getPluginSource(source string) (notationplugin.PluginSourceType, error) { + source = strings.TrimSpace(source) + // check file path + _, fileError := os.Stat(source) + if fileError == nil { + return notationplugin.TypeFile, nil + } + // check url + url, urlError := url.ParseRequestURI(source) + if urlError == nil { + if url.Scheme != "https" { + return notationplugin.TypeURL, fmt.Errorf("input plugin URL has to be in scheme HTTPS, but got %s", url.Scheme) + } + return notationplugin.TypeURL, nil + } + // unknown + fmt.Fprintf(os.Stderr, "%s is not a valid file: %v\n", source, fileError) + fmt.Fprintf(os.Stderr, "%s is not a valid URL: %v\n", source, urlError) + return notationplugin.TypeUnknown, fmt.Errorf("%s is an unknown plugin source", source) +} From 981cef3ae161c1da58e4e19d3a11b86d671df155 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 7 Nov 2023 16:19:54 +0800 Subject: [PATCH 14/83] fixed unit tests Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go index 8295aa8aa..f7bfc4354 100644 --- a/cmd/notation/internal/plugin/plugin_test.go +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -13,8 +13,8 @@ func TestCheckPluginExistence(t *testing.T) { } func TestValidateCheckSum(t *testing.T) { - expectedErrorMsg := "plugin checkSum does not match user input. User input is invalid, got e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - if err := ValidateCheckSum("./testdata/test", "invalid"); err == nil || err.Error() != expectedErrorMsg { + expectedErrorMsg := "plugin checksum does not match user input. Expecting abcd123" + if err := ValidateCheckSum("./testdata/test", "abcd123"); err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected err %s, got %v", expectedErrorMsg, err) } if err := ValidateCheckSum("./testdata/test", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); err != nil { @@ -27,26 +27,26 @@ func TestExtractPluginNameFromExecutableFileName(t *testing.T) { if err != nil { t.Fatalf("expected nil err, got %v", err) } - if pluginName != "my-pluing" { - t.Fatalf("expected plugin name my-plugin, got %s", pluginName) + if pluginName != "my-plugin" { + t.Fatalf("expected plugin name my-plugin, but got %s", pluginName) } pluginName, err = ExtractPluginNameFromExecutableFileName("notation-my-plugin.exe") if err != nil { t.Fatalf("expected nil err, got %v", err) } - if pluginName != "my-pluing" { - t.Fatalf("expected plugin name my-plugin, got %s", pluginName) + if pluginName != "my-plugin" { + t.Fatalf("expected plugin name my-plugin, but got %s", pluginName) } _, err = ExtractPluginNameFromExecutableFileName("myPlugin") - expectedErrorMsg := "invalid plugin executable file name. file name requires format notation-{plugin-name}, got myPlugin" + expectedErrorMsg := "invalid plugin executable file name. file name requires format notation-{plugin-name}, but got myPlugin" if err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected %s, got %v", expectedErrorMsg, err) } _, err = ExtractPluginNameFromExecutableFileName("my-plugin") - expectedErrorMsg = "invalid plugin executable file name. file name requires format notation-{plugin-name}, got my-plugin" + expectedErrorMsg = "invalid plugin executable file name. file name requires format notation-{plugin-name}, but got my-plugin" if err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected %s, got %v", expectedErrorMsg, err) } From e641eb59304c56d2f59d208138bffdc2e737acf4 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 7 Nov 2023 17:18:46 +0800 Subject: [PATCH 15/83] e2e tests Signed-off-by: Patrick Zheng --- test/e2e/internal/notation/init.go | 37 +++++++------ test/e2e/run.sh | 3 +- test/e2e/suite/plugin/install.go | 83 ++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 test/e2e/suite/plugin/install.go 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..2d0f85726 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -73,7 +73,7 @@ go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.9.5 # build e2e plugin 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 -czvf $PLUGIN_NAME.tar.gz ./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/install.go b/test/e2e/suite/plugin/install.go new file mode 100644 index 000000000..713ec7d40 --- /dev/null +++ b/test/e2e/suite/plugin/install.go @@ -0,0 +1,83 @@ +// 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" +) + +const ( + PluginURL = "https://github.com/notaryproject/notation-action/raw/e2e-test-plugin/tests/plugin_binaries/notation-e2e-test-plugin_0.1.0_linux_amd64.tar.gz" + PluginCheckSum = "be8d035024d3a96afb4118af32f2e201f126c7254b02f7bcffb3e3149d744fd2" +) + +var _ = Describe("notation plugin install", func() { + It("with invalid plugin source type", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", NotationE2EPluginPath). + MatchKeyWords("unknown plugin source") + }) + }) + + It("with valid plugin file path", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", NotationE2EPluginTarGzPath). + MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") + }) + }) + + It("with plugin already installed", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", NotationE2EPluginTarGzPath). + MatchContent("plugin e2e-plugin already installed\n") + }) + }) + + It("with plugin already installed but force install", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "--force"). + MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") + }) + }) + + It("with valid plugin URL but missing checksum", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", PluginURL). + MatchContent("install from URL requires non-empty SHA256 checksum of the plugin source") + }) + }) + + It("with invalid plugin URL scheme", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", "http://invalid", "--checksum", "abcd"). + MatchContent("input plugin URL has to be in scheme HTTPS, but got http") + }) + }) + + It("with invalid plugin URL", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", "https://invalid", "--checksum", "abcd"). + MatchKeyWords("failed to download plugin from URL https://invalid") + }) + }) + + It("with valid plugin URL", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", PluginURL, "--checksum", PluginCheckSum). + MatchContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") + }) + }) +}) From 7983bd9dbc41a6297fb22bbca8647b77511631da Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 7 Nov 2023 17:40:04 +0800 Subject: [PATCH 16/83] fixing e2e tests Signed-off-by: Patrick Zheng --- test/e2e/run.sh | 2 +- test/e2e/suite/plugin/install.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 2d0f85726..ab072d804 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -73,7 +73,7 @@ go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.9.5 # build e2e plugin PLUGIN_NAME=e2e-plugin -( cd $CWD/plugin && go build -o ./bin/$PLUGIN_NAME . && echo "e2e plugin built." && tar -czvf $PLUGIN_NAME.tar.gz ./bin/$PLUGIN_NAME) +( cd $CWD/plugin && go build -o ./bin/$PLUGIN_NAME . && echo "e2e plugin built." && tar -czvf ./bin/$PLUGIN_NAME.tar.gz ./bin/$PLUGIN_NAME) # setup registry case $REGISTRY_NAME in diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 713ec7d40..9828efedd 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -25,10 +25,10 @@ const ( ) var _ = Describe("notation plugin install", func() { - It("with invalid plugin source type", func() { + It("with invalid plugin file type", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", NotationE2EPluginPath). - MatchKeyWords("unknown plugin source") + MatchContent("Error: failed to install the plugin, invalid file format. Only support .tar.gz and .zip") }) }) From 56612a6b1bff25eac49b09ea7a5d8d9b538c28d8 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 7 Nov 2023 17:47:55 +0800 Subject: [PATCH 17/83] fixing e2e tests Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 9828efedd..a5e867f77 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -28,21 +28,21 @@ var _ = Describe("notation plugin install", func() { It("with invalid plugin file type", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", NotationE2EPluginPath). - MatchContent("Error: failed to install the plugin, invalid file format. Only support .tar.gz and .zip") + MatchErrContent("Error: failed to install the plugin, invalid file format. Only support .tar.gz and .zip") }) }) It("with valid plugin file path", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", NotationE2EPluginTarGzPath). - MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") + MatchErrContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") }) }) It("with plugin already installed", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", NotationE2EPluginTarGzPath). - MatchContent("plugin e2e-plugin already installed\n") + MatchErrContent("plugin e2e-plugin already installed\n") }) }) @@ -56,28 +56,28 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin URL but missing checksum", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", PluginURL). - MatchContent("install from URL requires non-empty SHA256 checksum of the plugin source") + MatchErrContent("install from URL requires non-empty SHA256 checksum of the plugin source") }) }) It("with invalid plugin URL scheme", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", "http://invalid", "--checksum", "abcd"). - MatchContent("input plugin URL has to be in scheme HTTPS, but got http") + MatchErrContent("input plugin URL has to be in scheme HTTPS, but got http") }) }) It("with invalid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", "https://invalid", "--checksum", "abcd"). - MatchKeyWords("failed to download plugin from URL https://invalid") + MatchErrKeyWords("failed to download plugin from URL https://invalid") }) }) It("with valid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", PluginURL, "--checksum", PluginCheckSum). - MatchContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") + MatchErrContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") }) }) }) From 1c070ee177021d39d1e31f592977bd5f6bc60e98 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 7 Nov 2023 17:54:51 +0800 Subject: [PATCH 18/83] fixing e2e tests Signed-off-by: Patrick Zheng --- test/e2e/run.sh | 2 +- test/e2e/suite/plugin/install.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/run.sh b/test/e2e/run.sh index ab072d804..034daa85e 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -73,7 +73,7 @@ go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.9.5 # build e2e plugin PLUGIN_NAME=e2e-plugin -( cd $CWD/plugin && go build -o ./bin/$PLUGIN_NAME . && echo "e2e plugin built." && tar -czvf ./bin/$PLUGIN_NAME.tar.gz ./bin/$PLUGIN_NAME) +( cd $CWD/plugin && go build -o ./bin/$PLUGIN_NAME . && echo "e2e plugin built." && tar -czvf ./bin/$PLUGIN_NAME.tar.gz -C ./bin/ $PLUGIN_NAME) # setup registry case $REGISTRY_NAME in diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index a5e867f77..0e9a77fff 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -35,7 +35,7 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin file path", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", NotationE2EPluginTarGzPath). - MatchErrContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") + MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") }) }) @@ -77,7 +77,7 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", PluginURL, "--checksum", PluginCheckSum). - MatchErrContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") + MatchContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") }) }) }) From 5d2d27c92ab6ee6aa9e44649b75ef2ff7b8f413b Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 8 Nov 2023 10:38:04 +0800 Subject: [PATCH 19/83] fixing e2e tests Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 8 ++++---- test/e2e/run.sh | 2 +- test/e2e/suite/plugin/install.go | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 364e8fd65..86761821f 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -103,7 +103,7 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { if opts.inputCheckSum == "" { return errors.New("install from URL requires non-empty SHA256 checksum of the plugin source") } - tmpDir, err := os.MkdirTemp(".", notationPluginTmpDir) + tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) if err != nil { return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) } @@ -182,7 +182,7 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error return err } } - return fmt.Errorf("no valid plugin executable file was found in %s", zipPath) + return fmt.Errorf("no valid plugin executable file was found in %s. Plugin executable file name must in format notation-{plugin-name}", zipPath) } // installPluginFromTarGz extracts and untar a plugin tar.gz file, validates and @@ -220,7 +220,7 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e return err } } - return fmt.Errorf("no valid plugin executable file was found in %s", tarGzPath) + return fmt.Errorf("no valid plugin executable file was found in %s. Plugin executable file name must in format notation-{plugin-name}", tarGzPath) } // installPluginExecutable extracts, validates, and installs a plugin from @@ -248,7 +248,7 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io } } // extract to tmp dir - tmpDir, err := os.MkdirTemp(".", notationPluginTmpDir) + tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) if err != nil { return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) } diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 034daa85e..c74347f1d 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -73,7 +73,7 @@ go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.9.5 # build e2e plugin PLUGIN_NAME=e2e-plugin -( cd $CWD/plugin && go build -o ./bin/$PLUGIN_NAME . && echo "e2e plugin built." && tar -czvf ./bin/$PLUGIN_NAME.tar.gz -C ./bin/ $PLUGIN_NAME) +( cd $CWD/plugin && go build -o ./bin/$PLUGIN_NAME . && echo "e2e plugin built." && tar -C ./bin/ -czvf ./bin/notation-$PLUGIN_NAME.tar.gz $PLUGIN_NAME) # setup registry case $REGISTRY_NAME in diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 0e9a77fff..e1f403aa4 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -27,21 +27,21 @@ const ( var _ = Describe("notation plugin install", func() { It("with invalid plugin file type", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", NotationE2EPluginPath). + notation.ExpectFailure().Exec("plugin", "install", NotationE2EPluginPath). MatchErrContent("Error: failed to install the plugin, invalid file format. Only support .tar.gz and .zip") }) }) It("with valid plugin file path", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", NotationE2EPluginTarGzPath). + notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "-v"). MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") }) }) It("with plugin already installed", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", NotationE2EPluginTarGzPath). + notation.ExpectFailure().Exec("plugin", "install", NotationE2EPluginTarGzPath). MatchErrContent("plugin e2e-plugin already installed\n") }) }) @@ -55,21 +55,21 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin URL but missing checksum", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", PluginURL). + notation.ExpectFailure().Exec("plugin", "install", PluginURL). MatchErrContent("install from URL requires non-empty SHA256 checksum of the plugin source") }) }) It("with invalid plugin URL scheme", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", "http://invalid", "--checksum", "abcd"). + notation.ExpectFailure().Exec("plugin", "install", "http://invalid", "--checksum", "abcd"). MatchErrContent("input plugin URL has to be in scheme HTTPS, but got http") }) }) It("with invalid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", "https://invalid", "--checksum", "abcd"). + notation.ExpectFailure().Exec("plugin", "install", "https://invalid", "--checksum", "abcd"). MatchErrKeyWords("failed to download plugin from URL https://invalid") }) }) From 26366bc4590e578a117b0c9cb6f26f2064c3a76d Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 8 Nov 2023 11:09:46 +0800 Subject: [PATCH 20/83] fixing e2e tests Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 16 +++++++--------- test/e2e/run.sh | 4 ++-- test/e2e/suite/plugin/install.go | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 86761821f..7f363a5f9 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -22,7 +22,6 @@ import ( "fmt" "io" "io/fs" - "net/url" "os" "path/filepath" "runtime" @@ -37,7 +36,10 @@ import ( "github.com/spf13/cobra" ) -const notationPluginTmpDir = "notationPluginTmpDir" +const ( + notationPluginTmpDir = "notationPluginTmpDir" + httpsPrefix = "https://" +) type pluginInstallOpts struct { cmd.LoggingFlagOpts @@ -297,15 +299,11 @@ func getPluginSource(source string) (notationplugin.PluginSourceType, error) { return notationplugin.TypeFile, nil } // check url - url, urlError := url.ParseRequestURI(source) - if urlError == nil { - if url.Scheme != "https" { - return notationplugin.TypeURL, fmt.Errorf("input plugin URL has to be in scheme HTTPS, but got %s", url.Scheme) - } + if strings.HasPrefix(source, httpsPrefix) { return notationplugin.TypeURL, nil } // unknown - fmt.Fprintf(os.Stderr, "%s is not a valid file: %v\n", source, fileError) - fmt.Fprintf(os.Stderr, "%s is not a valid URL: %v\n", source, urlError) + fmt.Fprintf(os.Stdout, "%s is not a valid file: %v\n", source, fileError) + fmt.Fprintf(os.Stdout, "%s is not a valid HTTPS URL\n", source) return notationplugin.TypeUnknown, fmt.Errorf("%s is an unknown plugin source", source) } diff --git a/test/e2e/run.sh b/test/e2e/run.sh index c74347f1d..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." && tar -C ./bin/ -czvf ./bin/notation-$PLUGIN_NAME.tar.gz $PLUGIN_NAME) +( 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 diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index e1f403aa4..2b5418a06 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -28,7 +28,7 @@ var _ = Describe("notation plugin install", func() { It("with invalid plugin file type", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", NotationE2EPluginPath). - MatchErrContent("Error: failed to install the plugin, invalid file format. Only support .tar.gz and .zip") + MatchErrContent("Error: failed to install the plugin, invalid file format. Only support .tar.gz and .zip\n") }) }) From 0e5a109317c1bc31dd5bdedd3249f922d8f044a3 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 8 Nov 2023 11:28:34 +0800 Subject: [PATCH 21/83] fixing e2e tests Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 6 +++--- test/e2e/suite/plugin/install.go | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 7f363a5f9..5b03e631c 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -303,7 +303,7 @@ func getPluginSource(source string) (notationplugin.PluginSourceType, error) { return notationplugin.TypeURL, nil } // unknown - fmt.Fprintf(os.Stdout, "%s is not a valid file: %v\n", source, fileError) - fmt.Fprintf(os.Stdout, "%s is not a valid HTTPS URL\n", source) - return notationplugin.TypeUnknown, fmt.Errorf("%s is an unknown plugin source", source) + fmt.Fprintf(os.Stdout, "%q is not a valid file: %v\n", source, fileError) + fmt.Fprintf(os.Stdout, "%q is not a valid HTTPS URL\n", source) + return notationplugin.TypeUnknown, fmt.Errorf("%q is an unknown plugin source. Require file path or HTTPS URL", source) } diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 2b5418a06..ec88a41a0 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -41,6 +41,9 @@ var _ = Describe("notation plugin install", func() { It("with plugin already installed", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "-v"). + MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") + notation.ExpectFailure().Exec("plugin", "install", NotationE2EPluginTarGzPath). MatchErrContent("plugin e2e-plugin already installed\n") }) @@ -56,14 +59,14 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin URL but missing checksum", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", PluginURL). - MatchErrContent("install from URL requires non-empty SHA256 checksum of the plugin source") + MatchErrContent("Error: install from URL requires non-empty SHA256 checksum of the plugin source\n") }) }) It("with invalid plugin URL scheme", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", "http://invalid", "--checksum", "abcd"). - MatchErrContent("input plugin URL has to be in scheme HTTPS, but got http") + MatchErrContent("Error: \"http://invalid\" is an unknown plugin source. Require file path or HTTPS URL\n") }) }) From 73d1aeb2901454e90eabce902a8bbc997c6e820c Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 8 Nov 2023 12:27:52 +0800 Subject: [PATCH 22/83] fixing e2e tests Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index ec88a41a0..21b5e6401 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -45,7 +45,7 @@ var _ = Describe("notation plugin install", func() { MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") notation.ExpectFailure().Exec("plugin", "install", NotationE2EPluginTarGzPath). - MatchErrContent("plugin e2e-plugin already installed\n") + MatchErrContent("Error: failed to install the plugin, plugin e2e-plugin already installed\n") }) }) From 4f21480bd173e66a0b813b9386693ef24a47f701 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 8 Nov 2023 13:27:40 +0800 Subject: [PATCH 23/83] clean up Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 46 ++++++++++++++++---------------- test/e2e/suite/plugin/install.go | 29 +++++++++++--------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 5b03e631c..e2fedd1cf 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -53,9 +53,9 @@ func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { opts = &pluginInstallOpts{} } command := &cobra.Command{ - Use: "install [flags] ", + Use: "install [flags] ", Short: "Install plugin", - Long: `Install a Notation plugin + Long: `Install a plugin Example - Install plugin from file system: notation plugin install wabbit-plugin-v1.0.zip @@ -70,7 +70,7 @@ Example - Install plugin from file system with .tar.gz: notation plugin install wabbit-plugin-v1.0.tar.gz Example - Install plugin from URL, SHA256 checksum is required: - notation plugin install --checksum abcxyz https://wabbit-networks.com/intaller/linux/amd64/wabbit-plugin-v1.0.tar.gz + notation plugin install https://wabbit-networks.com/intaller/linux/amd64/wabbit-plugin-v1.0.tar.gz --checksum abcxyz `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { @@ -84,8 +84,8 @@ Example - Install plugin from URL, SHA256 checksum is required: }, } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) - command.Flags().StringVar(&opts.inputCheckSum, "checksum", "", "if set, must match the SHA256 of the plugin source") - command.Flags().BoolVar(&opts.force, "force", false, "force to install and overwrite the plugin") + command.Flags().StringVar(&opts.inputCheckSum, "checksum", "", "must match SHA256 of the plugin source") + command.Flags().BoolVar(&opts.force, "force", false, "force the installation of a plugin") return command } @@ -120,6 +120,24 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { } } +// getPluginSource returns the type of plugin source +func getPluginSource(source string) (notationplugin.PluginSourceType, error) { + source = strings.TrimSpace(source) + // check file path + _, fileError := os.Stat(source) + if fileError == nil { + return notationplugin.TypeFile, nil + } + // check url + if strings.HasPrefix(source, httpsPrefix) { + return notationplugin.TypeURL, nil + } + // unknown + fmt.Fprintf(os.Stdout, "%q is not a valid file: %v\n", source, fileError) + fmt.Fprintf(os.Stdout, "%q is not a valid HTTPS URL\n", source) + return notationplugin.TypeUnknown, fmt.Errorf("%q is an unknown plugin source. Require file path or HTTPS URL", source) +} + // installFromFileSystem install the plugin from file system func installFromFileSystem(ctx context.Context, inputPath string, inputCheckSum string, force bool) error { // sanity check @@ -289,21 +307,3 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io fmt.Printf("Succussefully installed plugin %s, version %s\n", pluginName, pluginVersion) return nil } - -// getPluginSource returns the type of plugin source -func getPluginSource(source string) (notationplugin.PluginSourceType, error) { - source = strings.TrimSpace(source) - // check file path - _, fileError := os.Stat(source) - if fileError == nil { - return notationplugin.TypeFile, nil - } - // check url - if strings.HasPrefix(source, httpsPrefix) { - return notationplugin.TypeURL, nil - } - // unknown - fmt.Fprintf(os.Stdout, "%q is not a valid file: %v\n", source, fileError) - fmt.Fprintf(os.Stdout, "%q is not a valid HTTPS URL\n", source) - return notationplugin.TypeUnknown, fmt.Errorf("%q is an unknown plugin source. Require file path or HTTPS URL", source) -} diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 21b5e6401..2826bcc4a 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -25,17 +25,17 @@ const ( ) var _ = Describe("notation plugin install", func() { - It("with invalid plugin file type", func() { + It("with valid plugin file path", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.ExpectFailure().Exec("plugin", "install", NotationE2EPluginPath). - MatchErrContent("Error: failed to install the plugin, invalid file format. Only support .tar.gz and .zip\n") + notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "-v"). + MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") }) }) - It("with valid plugin file path", func() { + It("with invalid plugin file type", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "-v"). - MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") + notation.ExpectFailure().Exec("plugin", "install", NotationE2EPluginPath). + MatchErrContent("Error: failed to install the plugin, invalid file format. Only support .tar.gz and .zip\n") }) }) @@ -51,11 +51,21 @@ var _ = Describe("notation plugin install", func() { It("with plugin already installed but force install", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "-v"). + MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") + notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "--force"). MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") }) }) + It("with valid plugin URL", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.Exec("plugin", "install", PluginURL, "--checksum", PluginCheckSum). + MatchContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") + }) + }) + It("with valid plugin URL but missing checksum", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", PluginURL). @@ -76,11 +86,4 @@ var _ = Describe("notation plugin install", func() { MatchErrKeyWords("failed to download plugin from URL https://invalid") }) }) - - It("with valid plugin URL", func() { - Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", PluginURL, "--checksum", PluginCheckSum). - MatchContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") - }) - }) }) From e275eabd19a4e491f2c7fddc9046e88eaefae56a Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 8 Nov 2023 13:59:54 +0800 Subject: [PATCH 24/83] fix CI Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index e2fedd1cf..154901278 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -185,6 +185,9 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error } defer archive.Close() for _, f := range archive.File { + if strings.Contains(f.Name, "..") { + continue + } fmode := f.Mode() // requires one and only one executable file, with name in format // notation-{plugin-name}, exists in the zip file @@ -228,6 +231,9 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e } return err } + if strings.Contains(header.Name, "..") { + continue + } fmode := header.FileInfo().Mode() // requires one and only one executable file, with name in format // notation-{plugin-name}, exists in the tar.gz file From fccef51575671ef7d2544f956ba5aff248648c39 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 8 Nov 2023 14:03:52 +0800 Subject: [PATCH 25/83] added license headers Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 13 +++++++++++++ cmd/notation/internal/plugin/plugin_test.go | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index e074276f9..538b54667 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -1,3 +1,16 @@ +// 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 ( diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go index f7bfc4354..940f88f1b 100644 --- a/cmd/notation/internal/plugin/plugin_test.go +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -1,3 +1,16 @@ +// 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 ( From 31a7dc15d734fd5e9972b0ff738c00de0b8ed575 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 15 Nov 2023 09:23:05 +0800 Subject: [PATCH 26/83] updates Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 147 +++++++++++++------- cmd/notation/internal/plugin/plugin_test.go | 33 ++++- cmd/notation/plugin/install.go | 138 ++++++++++-------- go.mod | 2 +- internal/osutil/file.go | 11 +- 5 files changed, 211 insertions(+), 120 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 538b54667..06dc203b9 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -15,124 +15,163 @@ package plugin import ( "context" - "errors" "fmt" "io" "net/http" "os" - "path/filepath" "strings" "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/log" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/proto" notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" "github.com/notaryproject/notation/internal/osutil" + "github.com/notaryproject/notation/internal/trace" + "github.com/notaryproject/notation/internal/version" "github.com/opencontainers/go-digest" + "github.com/sirupsen/logrus" + "golang.org/x/mod/semver" + "oras.land/oras-go/v2/registry/remote/auth" ) // PluginSourceType is an enum for plugin source -type PluginSourceType string +type PluginSourceType int const ( - // TypeFile means plugin source is file - TypeFile PluginSourceType = "file" + // PluginSourceTypeFile means plugin source is file + PluginSourceTypeUnknown PluginSourceType = 1 + iota - // TypeURL means plugin source is URL - TypeURL PluginSourceType = "url" + // PluginSourceTypeFile means plugin source is file + PluginSourceTypeFile - // TypeUnknown means unknown plugin source - TypeUnknown PluginSourceType = "unknown" + // PluginSourceTypeURL means plugin source is URL + PluginSourceTypeURL ) const ( - // TypeZip means plugin file is zip - TypeZip = "application/zip" + // MediaTypeZip means plugin file is zip + MediaTypeZip = "application/zip" - // TypeGzip means plugin file is gzip - TypeGzip = "application/x-gzip" + // MediaTypeGzip means plugin file is gzip + MediaTypeGzip = "application/x-gzip" ) -// CheckPluginExistence returns true if a plugin already exists -func CheckPluginExistence(ctx context.Context, pluginName string) (bool, error) { +// GetPluginMetadataIfExist returns plugin's metadata if it exists in Notation +func GetPluginMetadataIfExist(ctx context.Context, pluginName string) (*proto.GetMetadataResponse, error) { mgr := plugin.NewCLIManager(dir.PluginFS()) - _, err := mgr.Get(ctx, pluginName) + plugin, err := mgr.Get(ctx, pluginName) if err != nil { - if errors.Is(err, os.ErrNotExist) { - return false, nil - } - return false, err + return nil, err } - return true, nil + return plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) +} + +// GetPluginMetadata returns plugin's metadata given plugin path +func GetPluginMetadata(ctx context.Context, pluginName, path string) (*proto.GetMetadataResponse, error) { + plugin, err := plugin.NewCLIPlugin(ctx, pluginName, path) + if err != nil { + return nil, err + } + return plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) +} + +// ComparePluginVersion validates and compares two plugin semantic versions +func ComparePluginVersion(v, w string) (int, error) { + // semantic version strings must begin with a leading "v" + if !strings.HasPrefix(v, "v") { + v = "v" + v + } + if !semver.IsValid(v) { + return 0, fmt.Errorf("%s is not a valid semantic version", v) + } + if !strings.HasPrefix(w, "v") { + w = "v" + w + } + if !semver.IsValid(w) { + return 0, fmt.Errorf("%s is not a valid semantic version", w) + } + return semver.Compare(v, w), nil } // ValidateCheckSum returns nil if SHA256 of file at path equals to checkSum. func ValidateCheckSum(path string, checkSum string) error { - r, err := os.Open(path) + rc, err := os.Open(path) if err != nil { return err } - defer r.Close() - dgst, err := digest.FromReader(r) + defer rc.Close() + dgst, err := digest.FromReader(rc) if err != nil { return err } enc := dgst.Encoded() - if enc != checkSum { + if enc != strings.ToLower(checkSum) { return fmt.Errorf("plugin checksum does not match user input. Expecting %s", checkSum) } return nil } -// ValidatePluginMetadata validates plugin metadata given plugin name and path, -// returns the plugin version on success -func ValidatePluginMetadata(ctx context.Context, pluginName, path string) (string, error) { - plugin, err := plugin.NewCLIPlugin(ctx, pluginName, path) - if err != nil { - return "", err - } - metadata, err := plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) - if err != nil { - return "", err - } - return metadata.Version, nil -} - // DownloadPluginFromURL downloads plugin file from url to a tmp directory // it returns the tmp file path of the downloaded file -func DownloadPluginFromURL(ctx context.Context, url, tmpDir string) (string, error) { - // Create the file - tmpFilePath := filepath.Join(tmpDir, "notationPluginTmp") - out, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) +func DownloadPluginFromURL(ctx context.Context, url string, tmpFile *os.File) error { + // Get the data + client := getClient(ctx) + req, err := http.NewRequest("GET", url, nil) if err != nil { - return "", err + return err } - defer out.Close() - // Get the data - resp, err := http.Get(url) + resp, err := client.Do(req) if err != nil { - return "", err + return err } defer resp.Body.Close() // Check server response if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("bad status: %s", resp.Status) + return fmt.Errorf("bad status: %s", resp.Status) } - // Writer the body to file - _, err = io.Copy(out, resp.Body) + // Write the body to file + _, err = io.Copy(tmpFile, resp.Body) if err != nil { - return "", err + return err } - return tmpFilePath, err + return nil } // ExtractPluginNameFromExecutableFileName gets plugin name from plugin // executable file name based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation func ExtractPluginNameFromExecutableFileName(execFileName string) (string, error) { fileName := osutil.FileNameWithoutExtension(execFileName) + if !strings.HasPrefix(fileName, proto.Prefix) { + return "", notationerrors.ErrorInvalidPluginName{Msg: fmt.Sprintf("invalid plugin executable file name. file name requires format notation-{plugin-name}, but got %s", fileName)} + } _, pluginName, found := strings.Cut(fileName, "-") - if !found || !strings.HasPrefix(fileName, proto.Prefix) { + if !found { return "", notationerrors.ErrorInvalidPluginName{Msg: fmt.Sprintf("invalid plugin executable file name. file name requires format notation-{plugin-name}, but got %s", fileName)} } return pluginName, nil } + +func setHttpDebugLog(ctx context.Context, authClient *auth.Client) { + if logrusLog, ok := log.GetLogger(ctx).(*logrus.Logger); ok && logrusLog.Level != logrus.DebugLevel { + return + } + if authClient.Client == nil { + authClient.Client = http.DefaultClient + } + if authClient.Client.Transport == nil { + authClient.Client.Transport = http.DefaultTransport + } + authClient.Client.Transport = trace.NewTransport(authClient.Client.Transport) +} + +// getClient returns an *auth.Client +func getClient(ctx context.Context) *auth.Client { + client := &auth.Client{ + Cache: auth.NewCache(), + ClientID: "notation", + } + client.SetUserAgent("notation/" + version.GetVersion()) + setHttpDebugLog(ctx, client) + return client +} diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go index 940f88f1b..8a62c7e83 100644 --- a/cmd/notation/internal/plugin/plugin_test.go +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -15,13 +15,38 @@ package plugin import ( "context" + "errors" + "os" "testing" ) -func TestCheckPluginExistence(t *testing.T) { - exist, err := CheckPluginExistence(context.Background(), "non-exist-plugin") - if exist || err != nil { - t.Fatalf("expected exist to be false with nil err, got: %v, %s", exist, err) +func TestGetPluginMetadataIfExist(t *testing.T) { + _, err := GetPluginMetadataIfExist(context.Background(), "non-exist-plugin") + if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("expected os.ErrNotExist err, got: %v", err) + } +} + +func TestComparePluginVersion(t *testing.T) { + comp, err := ComparePluginVersion("v1.0.0", "v1.0.1") + if err != nil || comp >= 0 { + t.Fatal("expected nil err and negative comp") + } + + comp, err = ComparePluginVersion("1.0.0", "1.0.1") + if err != nil || comp >= 0 { + t.Fatal("expected nil err and negative comp") + } + + comp, err = ComparePluginVersion("1.0.1", "1.0.1") + if err != nil || comp != 0 { + t.Fatal("expected nil err and comp equal to 0") + } + + expectedErrMsg := "vabc is not a valid semantic version" + _, err = ComparePluginVersion("abc", "1.0.1") + if err == nil || err.Error() != expectedErrMsg { + t.Fatalf("expected err %s, got %s", expectedErrMsg, err) } } diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 154901278..3154745dd 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "io/fs" + "net/url" "os" "path/filepath" "runtime" @@ -43,9 +44,12 @@ const ( type pluginInstallOpts struct { cmd.LoggingFlagOpts - pluginSource string - inputCheckSum string - force bool + pluginSourceType notationplugin.PluginSourceType + pluginSource string + inputCheckSum string + isFile bool + isUrl bool + force bool } func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { @@ -53,24 +57,24 @@ func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { opts = &pluginInstallOpts{} } command := &cobra.Command{ - Use: "install [flags] ", + Use: "install [flags] <--file|--url> ", Short: "Install plugin", Long: `Install a plugin Example - Install plugin from file system: - notation plugin install wabbit-plugin-v1.0.zip + notation plugin install --file wabbit-plugin-v1.0.zip Example - Install plugin from file system with user input SHA256 checksum: - notation plugin install wabbit-plugin-v1.0.zip --checksum abcdef + notation plugin install --file wabbit-plugin-v1.0.zip --checksum abcdef Example - Install plugin from file system regardless if it's already installed: - notation plugin install wabbit-plugin-v1.0.zip --force + notation plugin install --file wabbit-plugin-v1.0.zip --force Example - Install plugin from file system with .tar.gz: - notation plugin install wabbit-plugin-v1.0.tar.gz + notation plugin install --file wabbit-plugin-v1.0.tar.gz Example - Install plugin from URL, SHA256 checksum is required: - notation plugin install https://wabbit-networks.com/intaller/linux/amd64/wabbit-plugin-v1.0.tar.gz --checksum abcxyz + notation plugin install --url https://wabbit-networks.com/intaller/linux/amd64/wabbit-plugin-v1.0.tar.gz --checksum abcxyz `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { @@ -79,63 +83,65 @@ Example - Install plugin from URL, SHA256 checksum is required: opts.pluginSource = args[0] return nil }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if opts.isFile { + opts.pluginSourceType = notationplugin.PluginSourceTypeFile + return nil + } + if opts.isUrl { + opts.pluginSourceType = notationplugin.PluginSourceTypeURL + return nil + } + return errors.New("must choose one and only one flag from [--file, --url]") + }, RunE: func(cmd *cobra.Command, args []string) error { return installPlugin(cmd, opts) }, } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) + command.Flags().BoolVar(&opts.isFile, "file", false, "if set, install plugin from a file in file system") + command.Flags().BoolVar(&opts.isUrl, "url", false, "if set, install plugin from a URL") command.Flags().StringVar(&opts.inputCheckSum, "checksum", "", "must match SHA256 of the plugin source") command.Flags().BoolVar(&opts.force, "force", false, "force the installation of a plugin") + command.MarkFlagsMutuallyExclusive("file", "url") return command } func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { // set log level ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context()) - // get plugin source type, support file and URL - pluginSourceType, err := getPluginSource(opts.pluginSource) - if err != nil { - return err - } // core process - switch pluginSourceType { - case notationplugin.TypeFile: + switch opts.pluginSourceType { + case notationplugin.PluginSourceTypeFile: return installFromFileSystem(ctx, opts.pluginSource, opts.inputCheckSum, opts.force) - case notationplugin.TypeURL: + case notationplugin.PluginSourceTypeURL: if opts.inputCheckSum == "" { return errors.New("install from URL requires non-empty SHA256 checksum of the plugin source") } - tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) + url, err := url.Parse(opts.pluginSource) if err != nil { - return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) + return fmt.Errorf("failed to install from URL: %v", err) + } + if url.Scheme != "https" { + return fmt.Errorf("only support HTTPS URL, got %s", opts.pluginSource) } - defer os.RemoveAll(tmpDir) - downloadPath, err := notationplugin.DownloadPluginFromURL(ctx, opts.pluginSource, tmpDir) + tmpFile, err := os.CreateTemp(".", "notationPluginTmp") + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) + err = notationplugin.DownloadPluginFromURL(ctx, opts.pluginSource, tmpFile) if err != nil { return fmt.Errorf("failed to download plugin from URL %s with error: %w", opts.pluginSource, err) } + downloadPath, err := filepath.Abs(tmpFile.Name()) + if err != nil { + return err + } return installFromFileSystem(ctx, downloadPath, opts.inputCheckSum, opts.force) default: - return fmt.Errorf("failed to install the plugin, plugin source type %s is not supported", pluginSourceType) - } -} - -// getPluginSource returns the type of plugin source -func getPluginSource(source string) (notationplugin.PluginSourceType, error) { - source = strings.TrimSpace(source) - // check file path - _, fileError := os.Stat(source) - if fileError == nil { - return notationplugin.TypeFile, nil - } - // check url - if strings.HasPrefix(source, httpsPrefix) { - return notationplugin.TypeURL, nil + return errors.New("failed to install the plugin, plugin source type is unknown") } - // unknown - fmt.Fprintf(os.Stdout, "%q is not a valid file: %v\n", source, fileError) - fmt.Fprintf(os.Stdout, "%q is not a valid HTTPS URL\n", source) - return notationplugin.TypeUnknown, fmt.Errorf("%q is an unknown plugin source. Require file path or HTTPS URL", source) } // installFromFileSystem install the plugin from file system @@ -159,12 +165,12 @@ func installFromFileSystem(ctx context.Context, inputPath string, inputCheckSum return fmt.Errorf("failed to install the plugin, %w", err) } switch fileType { - case notationplugin.TypeZip: + case notationplugin.MediaTypeZip: if err := installPluginFromZip(ctx, inputPath, force); err != nil { return fmt.Errorf("failed to install the plugin, %w", err) } return nil - case notationplugin.TypeGzip: + case notationplugin.MediaTypeGzip: // when file is gzip, require to be tar if err := installPluginFromTarGz(ctx, inputPath, force); err != nil { return fmt.Errorf("failed to install the plugin, %w", err) @@ -212,12 +218,12 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error // installs the plugin func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) error { logger := log.GetLogger(ctx) - r, err := os.Open(tarGzPath) + rc, err := os.Open(tarGzPath) if err != nil { return err } - defer r.Close() - decompressedStream, err := gzip.NewReader(r) + defer rc.Close() + decompressedStream, err := gzip.NewReader(rc) if err != nil { return err } @@ -263,38 +269,52 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io if runtime.GOOS != "windows" && filepath.Ext(fileName) == ".exe" { return fmt.Errorf("on %s, plugin executable file name %s cannot have the '.exe' extension", runtime.GOOS, fileName) } - // check plugin existence - if !force { - existed, err := notationplugin.CheckPluginExistence(ctx, pluginName) - if err != nil { - return fmt.Errorf("failed to check plugin existence, %w", err) - } - if existed { - return fmt.Errorf("plugin %s already installed", pluginName) - } - } // extract to tmp dir - tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) + tmpDir, err := os.MkdirTemp(".", notationPluginTmpDir) if err != nil { return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) } defer os.RemoveAll(tmpDir) tmpFilePath := filepath.Join(tmpDir, fileName) - pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fmode) + pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } if _, err := io.Copy(pluginFile, fileReader); err != nil { + if err := pluginFile.Close(); err != nil { + return err + } return err } if err := pluginFile.Close(); err != nil { return err } - // validate plugin metadata - pluginVersion, err := notationplugin.ValidatePluginMetadata(ctx, pluginName, tmpFilePath) + // get plugin metadata + pluginMetadata, err := notationplugin.GetPluginMetadata(ctx, pluginName, tmpFilePath) if err != nil { return err } + pluginVersion := pluginMetadata.Version + // check plugin existence and version + if !force { + currentPluginMetadata, err := notationplugin.GetPluginMetadataIfExist(ctx, pluginName) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + if err == nil { // plugin already installed + comp, err := notationplugin.ComparePluginVersion(pluginVersion, currentPluginMetadata.Version) + if err != nil { + return err + } + if comp < 0 { + return fmt.Errorf("%s current version %s is larger than the installing version %s", pluginName, currentPluginMetadata.Version, pluginVersion) + } + if comp == 0 { + fmt.Printf("%s with version %s already installed\n", pluginName, currentPluginMetadata.Version) + return nil + } + } + } // install plugin pluginPath, err := dir.PluginFS().SysPath(pluginName) if err != nil { diff --git a/go.mod b/go.mod index 7abcc4d2e..298ad0948 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 + golang.org/x/mod v0.13.0 golang.org/x/term v0.13.0 oras.land/oras-go/v2 v2.3.1 ) @@ -26,7 +27,6 @@ require ( 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/sync v0.4.0 // indirect golang.org/x/sys v0.13.0 // indirect ) diff --git a/internal/osutil/file.go b/internal/osutil/file.go index d838ad7ed..1a49f5b6e 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -14,6 +14,7 @@ package osutil import ( + "errors" "fmt" "io" "io/fs" @@ -99,11 +100,17 @@ func IsRegularFile(path string) (bool, error) { // DetectFileType returns a file's content type given path func DetectFileType(path string) (string, error) { - f, err := os.ReadFile(path) + rc, err := os.Open(path) if err != nil { return "", err } - return http.DetectContentType(f), nil + defer rc.Close() + var header [512]byte + _, err = io.ReadFull(rc, header[:]) + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { + return "", err + } + return http.DetectContentType(header[:]), nil } // FileNameWithoutExtension returns the file name without extension. From 5f893dc610be3ec30c92e9075d28e68312b044ae Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 15 Nov 2023 09:00:56 +0800 Subject: [PATCH 27/83] fix: fix the license check (#826) Signed-off-by: Patrick Zheng --- internal/trace/context.go | 13 +++++++++++++ internal/trace/context_test.go | 13 +++++++++++++ internal/trace/transport.go | 13 +++++++++++++ internal/trace/transport_test.go | 13 +++++++++++++ test/e2e/internal/utils/exec.go | 13 +++++++++++++ 5 files changed, 65 insertions(+) diff --git a/internal/trace/context.go b/internal/trace/context.go index 50d59a26a..8563d73ee 100644 --- a/internal/trace/context.go +++ b/internal/trace/context.go @@ -1,3 +1,16 @@ +// 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. + // Copied and adapted from oras (https://github.com/oras-project/oras) /* Copyright The ORAS Authors. diff --git a/internal/trace/context_test.go b/internal/trace/context_test.go index 3d640b968..14e35976d 100644 --- a/internal/trace/context_test.go +++ b/internal/trace/context_test.go @@ -1,3 +1,16 @@ +// 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. + // Copied and adapted from oras (https://github.com/oras-project/oras) /* Copyright The ORAS Authors. diff --git a/internal/trace/transport.go b/internal/trace/transport.go index ec386bcd7..e75d334d0 100644 --- a/internal/trace/transport.go +++ b/internal/trace/transport.go @@ -1,3 +1,16 @@ +// 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. + // Copied and adapted from oras (https://github.com/oras-project/oras) /* Copyright The ORAS Authors. diff --git a/internal/trace/transport_test.go b/internal/trace/transport_test.go index 601f93072..58e6dfbc2 100644 --- a/internal/trace/transport_test.go +++ b/internal/trace/transport_test.go @@ -1,3 +1,16 @@ +// 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. + // Copied and adapted from oras (https://github.com/oras-project/oras) /* Copyright The ORAS Authors. diff --git a/test/e2e/internal/utils/exec.go b/test/e2e/internal/utils/exec.go index df1e2ca94..32307377a 100644 --- a/test/e2e/internal/utils/exec.go +++ b/test/e2e/internal/utils/exec.go @@ -1,3 +1,16 @@ +// 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. + // copied and adopted from https://github.com/oras-project/oras with // modification /* From d5a68ee89ac8c97e4239d00f38cc3c1312201654 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 15 Nov 2023 13:57:06 +0800 Subject: [PATCH 28/83] updates Signed-off-by: Patrick Zheng --- cmd/notation/internal/errors/errors.go | 8 +- cmd/notation/internal/plugin/plugin.go | 40 ++++----- cmd/notation/internal/plugin/plugin_test.go | 8 +- cmd/notation/plugin/install.go | 93 ++++++++++----------- internal/osutil/file.go | 8 +- test/e2e/suite/plugin/install.go | 26 +++--- 6 files changed, 84 insertions(+), 99 deletions(-) diff --git a/cmd/notation/internal/errors/errors.go b/cmd/notation/internal/errors/errors.go index 631e4107e..599b9c7a5 100644 --- a/cmd/notation/internal/errors/errors.go +++ b/cmd/notation/internal/errors/errors.go @@ -51,13 +51,13 @@ func (e ErrorExceedMaxSignatures) Error() string { return fmt.Sprintf("exceeded configured limit of max signatures %d to examine", e.MaxSignatures) } -// ErrorInvalidPluginName is used when a plugin executable file name does not -// follow the spec. -type ErrorInvalidPluginName struct { +// ErrorInvalidPluginFileName is used when a file name is not a valid plugin +// file name following the spec https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation. +type ErrorInvalidPluginFileName struct { Msg string } -func (e ErrorInvalidPluginName) Error() string { +func (e ErrorInvalidPluginFileName) Error() string { if e.Msg != "" { return e.Msg } diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 06dc203b9..57051eb7f 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -113,7 +113,6 @@ func ValidateCheckSum(path string, checkSum string) error { } // DownloadPluginFromURL downloads plugin file from url to a tmp directory -// it returns the tmp file path of the downloaded file func DownloadPluginFromURL(ctx context.Context, url string, tmpFile *os.File) error { // Get the data client := getClient(ctx) @@ -138,20 +137,18 @@ func DownloadPluginFromURL(ctx context.Context, url string, tmpFile *os.File) er return nil } -// ExtractPluginNameFromExecutableFileName gets plugin name from plugin -// executable file name based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation -func ExtractPluginNameFromExecutableFileName(execFileName string) (string, error) { - fileName := osutil.FileNameWithoutExtension(execFileName) - if !strings.HasPrefix(fileName, proto.Prefix) { - return "", notationerrors.ErrorInvalidPluginName{Msg: fmt.Sprintf("invalid plugin executable file name. file name requires format notation-{plugin-name}, but got %s", fileName)} - } - _, pluginName, found := strings.Cut(fileName, "-") - if !found { - return "", notationerrors.ErrorInvalidPluginName{Msg: fmt.Sprintf("invalid plugin executable file name. file name requires format notation-{plugin-name}, but got %s", fileName)} +// getClient returns an *auth.Client +func getClient(ctx context.Context) *auth.Client { + client := &auth.Client{ + Cache: auth.NewCache(), + ClientID: "notation", } - return pluginName, nil + client.SetUserAgent("notation/" + version.GetVersion()) + setHttpDebugLog(ctx, client) + return client } +// setHttpDebugLog sets http debug logger func setHttpDebugLog(ctx context.Context, authClient *auth.Client) { if logrusLog, ok := log.GetLogger(ctx).(*logrus.Logger); ok && logrusLog.Level != logrus.DebugLevel { return @@ -165,13 +162,16 @@ func setHttpDebugLog(ctx context.Context, authClient *auth.Client) { authClient.Client.Transport = trace.NewTransport(authClient.Client.Transport) } -// getClient returns an *auth.Client -func getClient(ctx context.Context) *auth.Client { - client := &auth.Client{ - Cache: auth.NewCache(), - ClientID: "notation", +// ExtractPluginNameFromFileName checks if fileName is a valid plugin file name +// and gets plugin name from it based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation +func ExtractPluginNameFromFileName(fileName string) (string, error) { + fname := osutil.FileNameWithoutExtension(fileName) + if !strings.HasPrefix(fname, proto.Prefix) { + return "", notationerrors.ErrorInvalidPluginFileName{Msg: fmt.Sprintf("invalid plugin file name. Plugin file name requires format notation-{plugin-name}, but got %s", fname)} } - client.SetUserAgent("notation/" + version.GetVersion()) - setHttpDebugLog(ctx, client) - return client + _, pluginName, found := strings.Cut(fname, "-") + if !found { + return "", notationerrors.ErrorInvalidPluginFileName{Msg: fmt.Sprintf("invalid plugin file name. Plugin file name requires format notation-{plugin-name}, but got %s", fname)} + } + return pluginName, nil } diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go index 8a62c7e83..bc011f1d6 100644 --- a/cmd/notation/internal/plugin/plugin_test.go +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -61,7 +61,7 @@ func TestValidateCheckSum(t *testing.T) { } func TestExtractPluginNameFromExecutableFileName(t *testing.T) { - pluginName, err := ExtractPluginNameFromExecutableFileName("notation-my-plugin") + pluginName, err := ExtractPluginNameFromFileName("notation-my-plugin") if err != nil { t.Fatalf("expected nil err, got %v", err) } @@ -69,7 +69,7 @@ func TestExtractPluginNameFromExecutableFileName(t *testing.T) { t.Fatalf("expected plugin name my-plugin, but got %s", pluginName) } - pluginName, err = ExtractPluginNameFromExecutableFileName("notation-my-plugin.exe") + pluginName, err = ExtractPluginNameFromFileName("notation-my-plugin.exe") if err != nil { t.Fatalf("expected nil err, got %v", err) } @@ -77,13 +77,13 @@ func TestExtractPluginNameFromExecutableFileName(t *testing.T) { t.Fatalf("expected plugin name my-plugin, but got %s", pluginName) } - _, err = ExtractPluginNameFromExecutableFileName("myPlugin") + _, err = ExtractPluginNameFromFileName("myPlugin") expectedErrorMsg := "invalid plugin executable file name. file name requires format notation-{plugin-name}, but got myPlugin" if err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected %s, got %v", expectedErrorMsg, err) } - _, err = ExtractPluginNameFromExecutableFileName("my-plugin") + _, err = ExtractPluginNameFromFileName("my-plugin") expectedErrorMsg = "invalid plugin executable file name. file name requires format notation-{plugin-name}, but got my-plugin" if err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected %s, got %v", expectedErrorMsg, err) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 3154745dd..0dce810e2 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" "io" - "io/fs" "net/url" "os" "path/filepath" @@ -30,7 +29,6 @@ import ( "github.com/notaryproject/notation-go/dir" "github.com/notaryproject/notation-go/log" - notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" notationplugin "github.com/notaryproject/notation/cmd/notation/internal/plugin" "github.com/notaryproject/notation/internal/cmd" "github.com/notaryproject/notation/internal/osutil" @@ -39,7 +37,6 @@ import ( const ( notationPluginTmpDir = "notationPluginTmpDir" - httpsPrefix = "https://" ) type pluginInstallOpts struct { @@ -123,9 +120,9 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { return fmt.Errorf("failed to install from URL: %v", err) } if url.Scheme != "https" { - return fmt.Errorf("only support HTTPS URL, got %s", opts.pluginSource) + return fmt.Errorf("failed to install from URL: %q scheme is not HTTPS", opts.pluginSource) } - tmpFile, err := os.CreateTemp(".", "notationPluginTmp") + tmpFile, err := os.CreateTemp(".", "notationPluginDownloadTmp") if err != nil { return err } @@ -140,7 +137,7 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { } return installFromFileSystem(ctx, downloadPath, opts.inputCheckSum, opts.force) default: - return errors.New("failed to install the plugin, plugin source type is unknown") + return errors.New("failed to install the plugin: plugin source type is unknown") } } @@ -149,35 +146,36 @@ func installFromFileSystem(ctx context.Context, inputPath string, inputCheckSum // sanity check inputFileStat, err := os.Stat(inputPath) if err != nil { - return fmt.Errorf("failed to install the plugin, %w", err) + return fmt.Errorf("failed to install the plugin: %w", err) } if !inputFileStat.Mode().IsRegular() { - return fmt.Errorf("failed to install the plugin, %s is not a regular file", inputPath) + return fmt.Errorf("failed to install the plugin: %s is not a regular file", inputPath) } + // checksum check if inputCheckSum != "" { if err := notationplugin.ValidateCheckSum(inputPath, inputCheckSum); err != nil { - return fmt.Errorf("failed to install the plugin, %w", err) + return fmt.Errorf("failed to install the plugin: %w", err) } } // install the plugin based on file type fileType, err := osutil.DetectFileType(inputPath) if err != nil { - return fmt.Errorf("failed to install the plugin, %w", err) + return fmt.Errorf("failed to install the plugin: %w", err) } switch fileType { case notationplugin.MediaTypeZip: if err := installPluginFromZip(ctx, inputPath, force); err != nil { - return fmt.Errorf("failed to install the plugin, %w", err) + return fmt.Errorf("failed to install the plugin: %w", err) } return nil case notationplugin.MediaTypeGzip: // when file is gzip, require to be tar if err := installPluginFromTarGz(ctx, inputPath, force); err != nil { - return fmt.Errorf("failed to install the plugin, %w", err) + return fmt.Errorf("failed to install the plugin: %w", err) } return nil default: - return errors.New("failed to install the plugin, invalid file format. Only support .tar.gz and .zip") + return errors.New("failed to install the plugin: invalid file format. Only support .tar.gz and .zip") } } @@ -190,28 +188,26 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error return err } defer archive.Close() + // require one and only one file with name in the format + // `notation-{plugin-name}` for _, f := range archive.File { - if strings.Contains(f.Name, "..") { + if !f.Mode().IsRegular() || strings.Contains(f.Name, "..") { continue } - fmode := f.Mode() - // requires one and only one executable file, with name in format - // notation-{plugin-name}, exists in the zip file - if fmode.IsRegular() && osutil.IsOwnerExecutalbeFile(fmode) { - fileInArchive, err := f.Open() - if err != nil { - return err - } - defer fileInArchive.Close() - err = installPluginExecutable(ctx, f.Name, fileInArchive, fmode, force) - if errors.As(err, ¬ationerrors.ErrorInvalidPluginName{}) { - logger.Warnln(err) - continue - } + // validate and get plugin name from file name + pluginName, err := notationplugin.ExtractPluginNameFromFileName(f.Name) + if err != nil { + logger.Infoln(err) + continue + } + fileInArchive, err := f.Open() + if err != nil { return err } + defer fileInArchive.Close() + return installPluginExecutable(ctx, f.Name, pluginName, fileInArchive, force) } - return fmt.Errorf("no valid plugin executable file was found in %s. Plugin executable file name must in format notation-{plugin-name}", zipPath) + return fmt.Errorf("no valid plugin file was found in %s. Plugin file name must in format notation-{plugin-name}", zipPath) } // installPluginFromTarGz extracts and untar a plugin tar.gz file, validates and @@ -229,6 +225,8 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e } defer decompressedStream.Close() tarReader := tar.NewReader(decompressedStream) + // require one and only one file with name in the format + // `notation-{plugin-name}` for { header, err := tarReader.Next() if err != nil { @@ -237,42 +235,33 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e } return err } - if strings.Contains(header.Name, "..") { + if !header.FileInfo().Mode().IsRegular() || strings.Contains(header.Name, "..") { continue } - fmode := header.FileInfo().Mode() - // requires one and only one executable file, with name in format - // notation-{plugin-name}, exists in the tar.gz file - if fmode.IsRegular() && osutil.IsOwnerExecutalbeFile(fmode) { - err := installPluginExecutable(ctx, header.Name, tarReader, fmode, force) - if errors.As(err, ¬ationerrors.ErrorInvalidPluginName{}) { - logger.Warnln(err) - continue - } - return err + // validate and get plugin name from file name + pluginName, err := notationplugin.ExtractPluginNameFromFileName(header.Name) + if err != nil { + logger.Infoln(err) + continue } + return installPluginExecutable(ctx, header.Name, pluginName, tarReader, force) } - return fmt.Errorf("no valid plugin executable file was found in %s. Plugin executable file name must in format notation-{plugin-name}", tarGzPath) + return fmt.Errorf("no valid plugin file was found in %s. Plugin file name must in format notation-{plugin-name}", tarGzPath) } -// installPluginExecutable extracts, validates, and installs a plugin from -// reader -func installPluginExecutable(ctx context.Context, fileName string, fileReader io.Reader, fmode fs.FileMode, force bool) error { +// installPluginExecutable extracts, validates, and installs a plugin file +func installPluginExecutable(ctx context.Context, fileName string, pluginName string, fileReader io.Reader, force bool) error { // sanity check - pluginName, err := notationplugin.ExtractPluginNameFromExecutableFileName(fileName) - if err != nil { - return err - } if runtime.GOOS == "windows" && filepath.Ext(fileName) != ".exe" { - return fmt.Errorf("on Windows, plugin executable file name %s is missing the '.exe' extension", fileName) + return fmt.Errorf("on Windows, plugin executable file %s is missing the '.exe' extension", fileName) } if runtime.GOOS != "windows" && filepath.Ext(fileName) == ".exe" { - return fmt.Errorf("on %s, plugin executable file name %s cannot have the '.exe' extension", runtime.GOOS, fileName) + return fmt.Errorf("on %s, plugin executable file %s cannot have the '.exe' extension", runtime.GOOS, fileName) } // extract to tmp dir tmpDir, err := os.MkdirTemp(".", notationPluginTmpDir) if err != nil { - return fmt.Errorf("failed to create notationPluginTmpDir, %w", err) + return fmt.Errorf("failed to create notationPluginTmpDir: %w", err) } defer os.RemoveAll(tmpDir) tmpFilePath := filepath.Join(tmpDir, fileName) @@ -310,6 +299,8 @@ func installPluginExecutable(ctx context.Context, fileName string, fileReader io return fmt.Errorf("%s current version %s is larger than the installing version %s", pluginName, currentPluginMetadata.Version, pluginVersion) } if comp == 0 { + // if version is the same, no action is needed and no error is + // returned fmt.Printf("%s with version %s already installed\n", pluginName, currentPluginMetadata.Version) return nil } diff --git a/internal/osutil/file.go b/internal/osutil/file.go index 1a49f5b6e..9f0bca006 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -14,7 +14,6 @@ package osutil import ( - "errors" "fmt" "io" "io/fs" @@ -107,7 +106,7 @@ func DetectFileType(path string) (string, error) { defer rc.Close() var header [512]byte _, err = io.ReadFull(rc, header[:]) - if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { + if err != nil { return "", err } return http.DetectContentType(header[:]), nil @@ -121,8 +120,3 @@ func FileNameWithoutExtension(inputName string) string { fileName := filepath.Base(inputName) return strings.TrimSuffix(fileName, filepath.Ext(fileName)) } - -// IsOwnerExecutalbeFile checks whether file is owner executable -func IsOwnerExecutalbeFile(fmode fs.FileMode) bool { - return fmode&0100 != 0 -} diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 2826bcc4a..5f8049814 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -27,62 +27,62 @@ const ( var _ = Describe("notation plugin install", func() { It("with valid plugin file path", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "-v"). + notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath, "-v"). MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") }) }) It("with invalid plugin file type", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.ExpectFailure().Exec("plugin", "install", NotationE2EPluginPath). - MatchErrContent("Error: failed to install the plugin, invalid file format. Only support .tar.gz and .zip\n") + notation.ExpectFailure().Exec("plugin", "install", "file", NotationE2EPluginPath). + MatchErrContent("Error: failed to install the plugin: invalid file format. Only support .tar.gz and .zip\n") }) }) It("with plugin already installed", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "-v"). + notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath, "-v"). MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") - notation.ExpectFailure().Exec("plugin", "install", NotationE2EPluginTarGzPath). - MatchErrContent("Error: failed to install the plugin, plugin e2e-plugin already installed\n") + notation.ExpectFailure().Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). + MatchContent("e2e-plugin with version 1.0.0 already installed\n") }) }) It("with plugin already installed but force install", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "-v"). + notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath, "-v"). MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") - notation.Exec("plugin", "install", NotationE2EPluginTarGzPath, "--force"). + notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath, "--force"). MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") }) }) It("with valid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", PluginURL, "--checksum", PluginCheckSum). + notation.Exec("plugin", "install", "--url", PluginURL, "--checksum", PluginCheckSum). MatchContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") }) }) It("with valid plugin URL but missing checksum", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.ExpectFailure().Exec("plugin", "install", PluginURL). + notation.ExpectFailure().Exec("plugin", "install", "--url", PluginURL). MatchErrContent("Error: install from URL requires non-empty SHA256 checksum of the plugin source\n") }) }) It("with invalid plugin URL scheme", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.ExpectFailure().Exec("plugin", "install", "http://invalid", "--checksum", "abcd"). - MatchErrContent("Error: \"http://invalid\" is an unknown plugin source. Require file path or HTTPS URL\n") + notation.ExpectFailure().Exec("plugin", "install", "--url", "http://invalid", "--checksum", "abcd"). + MatchErrContent("Error: failed to install from URL: \"http://invalid\" scheme is not HTTPS\n") }) }) It("with invalid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.ExpectFailure().Exec("plugin", "install", "https://invalid", "--checksum", "abcd"). + notation.ExpectFailure().Exec("plugin", "install", "--url", "https://invalid", "--checksum", "abcd"). MatchErrKeyWords("failed to download plugin from URL https://invalid") }) }) From ff41ceb52f8ee7813a47b8675184a980f1fb44d6 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 15 Nov 2023 14:05:04 +0800 Subject: [PATCH 29/83] fixed tests Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin_test.go | 4 ++-- internal/osutil/file_test.go | 10 ---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go index bc011f1d6..d958d5746 100644 --- a/cmd/notation/internal/plugin/plugin_test.go +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -78,13 +78,13 @@ func TestExtractPluginNameFromExecutableFileName(t *testing.T) { } _, err = ExtractPluginNameFromFileName("myPlugin") - expectedErrorMsg := "invalid plugin executable file name. file name requires format notation-{plugin-name}, but got myPlugin" + expectedErrorMsg := "invalid plugin file name. Plugin file name requires format notation-{plugin-name}, but got myPlugin" if err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected %s, got %v", expectedErrorMsg, err) } _, err = ExtractPluginNameFromFileName("my-plugin") - expectedErrorMsg = "invalid plugin executable file name. file name requires format notation-{plugin-name}, but got my-plugin" + expectedErrorMsg = "invalid plugin file name. Plugin file name requires format notation-{plugin-name}, but got my-plugin" if err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected %s, got %v", expectedErrorMsg, err) } diff --git a/internal/osutil/file_test.go b/internal/osutil/file_test.go index abfe76ca0..12ef64d5c 100644 --- a/internal/osutil/file_test.go +++ b/internal/osutil/file_test.go @@ -15,7 +15,6 @@ package osutil import ( "bytes" - "io/fs" "io/ioutil" "os" "path/filepath" @@ -270,12 +269,3 @@ func TestFileNameWithoutExtension(t *testing.T) { t.Errorf("expected '%s', but got '%s'", expectedOutput, actualOutput) } } - -func TestIsOwnerExecutalbeFile(t *testing.T) { - input := fs.FileMode(0755) - expectedOutput := true - actualOutput := IsOwnerExecutalbeFile(input) - if actualOutput != expectedOutput { - t.Errorf("expected '%t', but got '%t'", expectedOutput, actualOutput) - } -} From 320b2e3e1159de5742c09a51af02576b7a70158d Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 15 Nov 2023 14:35:16 +0800 Subject: [PATCH 30/83] updates Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 0dce810e2..2a1f7da37 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -265,7 +265,7 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st } defer os.RemoveAll(tmpDir) tmpFilePath := filepath.Join(tmpDir, fileName) - pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700) if err != nil { return err } From f8747a43ad57655e4a7ca2886ad2d6751660b757 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 15 Nov 2023 14:41:53 +0800 Subject: [PATCH 31/83] updates Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 5f8049814..83c5288dd 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -34,17 +34,17 @@ var _ = Describe("notation plugin install", func() { It("with invalid plugin file type", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.ExpectFailure().Exec("plugin", "install", "file", NotationE2EPluginPath). + notation.ExpectFailure().Exec("plugin", "install", "--file", NotationE2EPluginPath). MatchErrContent("Error: failed to install the plugin: invalid file format. Only support .tar.gz and .zip\n") }) }) It("with plugin already installed", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath, "-v"). + notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") - notation.ExpectFailure().Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). + notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). MatchContent("e2e-plugin with version 1.0.0 already installed\n") }) }) From c400f1043f2e7876cc726cd3782e37b404d944de Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 16 Nov 2023 11:12:00 +0800 Subject: [PATCH 32/83] updates Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 7 ++++++- cmd/notation/plugin/install.go | 5 ++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 57051eb7f..bde71ca27 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -35,6 +35,10 @@ import ( "oras.land/oras-go/v2/registry/remote/auth" ) +// maxPluginSourceBytes specifies the limit on how many response +// bytes are allowed in the server's response to the download from URL request +var maxPluginSourceBytes int64 = 256 * 1024 * 1024 // 256 MiB + // PluginSourceType is an enum for plugin source type PluginSourceType int @@ -130,7 +134,8 @@ func DownloadPluginFromURL(ctx context.Context, url string, tmpFile *os.File) er return fmt.Errorf("bad status: %s", resp.Status) } // Write the body to file - _, err = io.Copy(tmpFile, resp.Body) + lr := io.LimitReader(resp.Body, maxPluginSourceBytes) + _, err = io.Copy(tmpFile, lr) if err != nil { return err } diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 2a1f7da37..385a616ac 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -127,6 +127,7 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { return err } defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() err = notationplugin.DownloadPluginFromURL(ctx, opts.pluginSource, tmpFile) if err != nil { return fmt.Errorf("failed to download plugin from URL %s with error: %w", opts.pluginSource, err) @@ -270,9 +271,7 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st return err } if _, err := io.Copy(pluginFile, fileReader); err != nil { - if err := pluginFile.Close(); err != nil { - return err - } + defer pluginFile.Close() return err } if err := pluginFile.Close(); err != nil { From 327172e7756fb8543b25792ecdda32a57a0f236b Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 16 Nov 2023 11:56:43 +0800 Subject: [PATCH 33/83] updates Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 385a616ac..05cf4ece4 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -97,8 +97,8 @@ Example - Install plugin from URL, SHA256 checksum is required: } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) command.Flags().BoolVar(&opts.isFile, "file", false, "if set, install plugin from a file in file system") - command.Flags().BoolVar(&opts.isUrl, "url", false, "if set, install plugin from a URL") - command.Flags().StringVar(&opts.inputCheckSum, "checksum", "", "must match SHA256 of the plugin source") + command.Flags().BoolVar(&opts.isUrl, "url", false, " if set, install plugin from a HTTPS URL") + command.Flags().StringVar(&opts.inputCheckSum, "sha256sum", "", "must match SHA256 of the plugin source") command.Flags().BoolVar(&opts.force, "force", false, "force the installation of a plugin") command.MarkFlagsMutuallyExclusive("file", "url") return command @@ -295,12 +295,12 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st return err } if comp < 0 { - return fmt.Errorf("%s current version %s is larger than the installing version %s", pluginName, currentPluginMetadata.Version, pluginVersion) + return fmt.Errorf("%s current version %s is larger than the installing version %s. To view a list of installed plugins, use `notation plugin list`", pluginName, currentPluginMetadata.Version, pluginVersion) } if comp == 0 { // if version is the same, no action is needed and no error is // returned - fmt.Printf("%s with version %s already installed\n", pluginName, currentPluginMetadata.Version) + fmt.Printf("Plugin %s with version %s is already installed. To view a list of installed plugins, use `notation plugin list`.\n", pluginName, currentPluginMetadata.Version) return nil } } From ba587a3744bbb4b0d19369a895af39d5da0cc74d Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 16 Nov 2023 11:58:31 +0800 Subject: [PATCH 34/83] updates Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 83c5288dd..15ebbc1a8 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -45,7 +45,7 @@ var _ = Describe("notation plugin install", func() { MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). - MatchContent("e2e-plugin with version 1.0.0 already installed\n") + MatchContent("Plugin e2e-plugin with version 1.0.0 is already installed. To view a list of installed plugins, use `notation plugin list`.\n") }) }) From 93da0fc125efc186b378905678f2959330776cb9 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 16 Nov 2023 12:01:32 +0800 Subject: [PATCH 35/83] updates e2e Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 15ebbc1a8..beec6bd3b 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -61,7 +61,7 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", "--url", PluginURL, "--checksum", PluginCheckSum). + notation.Exec("plugin", "install", "--url", PluginURL, "--sha256sum", PluginCheckSum). MatchContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") }) }) @@ -75,14 +75,14 @@ var _ = Describe("notation plugin install", func() { It("with invalid plugin URL scheme", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.ExpectFailure().Exec("plugin", "install", "--url", "http://invalid", "--checksum", "abcd"). + notation.ExpectFailure().Exec("plugin", "install", "--url", "http://invalid", "--sha256sum", "abcd"). MatchErrContent("Error: failed to install from URL: \"http://invalid\" scheme is not HTTPS\n") }) }) It("with invalid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.ExpectFailure().Exec("plugin", "install", "--url", "https://invalid", "--checksum", "abcd"). + notation.ExpectFailure().Exec("plugin", "install", "--url", "https://invalid", "--sha256sum", "abcd"). MatchErrKeyWords("failed to download plugin from URL https://invalid") }) }) From 5dcb505a9a6a2dc9733c857af7646eee5acaa9ea Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 17 Nov 2023 11:03:16 +0800 Subject: [PATCH 36/83] updates Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 18 ++++++++++++------ test/e2e/suite/plugin/install.go | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 05cf4ece4..4d32f2220 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -96,8 +96,8 @@ Example - Install plugin from URL, SHA256 checksum is required: }, } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) - command.Flags().BoolVar(&opts.isFile, "file", false, "if set, install plugin from a file in file system") - command.Flags().BoolVar(&opts.isUrl, "url", false, " if set, install plugin from a HTTPS URL") + command.Flags().BoolVar(&opts.isFile, "file", false, "install plugin from a file in file system") + command.Flags().BoolVar(&opts.isUrl, "url", false, " install plugin from a HTTPS URL") command.Flags().StringVar(&opts.inputCheckSum, "sha256sum", "", "must match SHA256 of the plugin source") command.Flags().BoolVar(&opts.force, "force", false, "force the installation of a plugin") command.MarkFlagsMutuallyExclusive("file", "url") @@ -284,23 +284,25 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st } pluginVersion := pluginMetadata.Version // check plugin existence and version + var currentPluginVersion string if !force { currentPluginMetadata, err := notationplugin.GetPluginMetadataIfExist(ctx, pluginName) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } + currentPluginVersion = currentPluginMetadata.Version if err == nil { // plugin already installed - comp, err := notationplugin.ComparePluginVersion(pluginVersion, currentPluginMetadata.Version) + comp, err := notationplugin.ComparePluginVersion(pluginVersion, currentPluginVersion) if err != nil { return err } if comp < 0 { - return fmt.Errorf("%s current version %s is larger than the installing version %s. To view a list of installed plugins, use `notation plugin list`", pluginName, currentPluginMetadata.Version, pluginVersion) + return fmt.Errorf("%s current version %s is higher than the installing version %s.\n To view a list of installed plugins, use `notation plugin list`. If you want to install an old version, use \"--force\" to force the installation", pluginName, currentPluginVersion, pluginVersion) } if comp == 0 { // if version is the same, no action is needed and no error is // returned - fmt.Printf("Plugin %s with version %s is already installed. To view a list of installed plugins, use `notation plugin list`.\n", pluginName, currentPluginMetadata.Version) + fmt.Printf("Plugin %s with version %s already exists.\n To view a list of installed plugins, use `notation plugin list`.\n", pluginName, currentPluginVersion) return nil } } @@ -320,6 +322,10 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st if err != nil { return err } - fmt.Printf("Succussefully installed plugin %s, version %s\n", pluginName, pluginVersion) + if currentPluginVersion != "" { + fmt.Printf("Succussefully installed plugin %s, updated the version from %s to %s\n", pluginName, currentPluginVersion, pluginVersion) + } else { + fmt.Printf("Succussefully installed plugin %s, version %s\n", pluginName, pluginVersion) + } return nil } diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index beec6bd3b..a1a7486a1 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -45,7 +45,7 @@ var _ = Describe("notation plugin install", func() { MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). - MatchContent("Plugin e2e-plugin with version 1.0.0 is already installed. To view a list of installed plugins, use `notation plugin list`.\n") + MatchContent("Plugin e2e-plugin with version 1.0.0 already exists.\n To view a list of installed plugins, use `notation plugin list`.\n") }) }) From 3618d4bc798fc5ca393e63afac282d13755d34c8 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 17 Nov 2023 11:20:07 +0800 Subject: [PATCH 37/83] fixed bug Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 16 ++++++++-------- test/e2e/suite/plugin/install.go | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 4d32f2220..d9f96537f 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -285,24 +285,24 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st pluginVersion := pluginMetadata.Version // check plugin existence and version var currentPluginVersion string - if !force { - currentPluginMetadata, err := notationplugin.GetPluginMetadataIfExist(ctx, pluginName) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } + currentPluginMetadata, err := notationplugin.GetPluginMetadataIfExist(ctx, pluginName) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + if err == nil { // plugin already exists currentPluginVersion = currentPluginMetadata.Version - if err == nil { // plugin already installed + if !force { comp, err := notationplugin.ComparePluginVersion(pluginVersion, currentPluginVersion) if err != nil { return err } if comp < 0 { - return fmt.Errorf("%s current version %s is higher than the installing version %s.\n To view a list of installed plugins, use `notation plugin list`. If you want to install an old version, use \"--force\" to force the installation", pluginName, currentPluginVersion, pluginVersion) + return fmt.Errorf("%s current version %s is higher than the installing version %s.\nTo view a list of installed plugins, use `notation plugin list`. If you want to install an old version, use \"--force\" to force the installation", pluginName, currentPluginVersion, pluginVersion) } if comp == 0 { // if version is the same, no action is needed and no error is // returned - fmt.Printf("Plugin %s with version %s already exists.\n To view a list of installed plugins, use `notation plugin list`.\n", pluginName, currentPluginVersion) + fmt.Printf("Plugin %s with version %s already exists.\nTo view a list of installed plugins, use `notation plugin list`.\n", pluginName, currentPluginVersion) return nil } } diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index a1a7486a1..9514cd8e6 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -45,7 +45,7 @@ var _ = Describe("notation plugin install", func() { MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). - MatchContent("Plugin e2e-plugin with version 1.0.0 already exists.\n To view a list of installed plugins, use `notation plugin list`.\n") + MatchContent("Plugin e2e-plugin with version 1.0.0 already exists.\nTo view a list of installed plugins, use `notation plugin list`.\n") }) }) From 1375a85a9026a56cf5b4f6a1148ed3c485fc3c79 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 17 Nov 2023 11:23:26 +0800 Subject: [PATCH 38/83] fix e2e Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 9514cd8e6..6fb44f81f 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -55,7 +55,7 @@ var _ = Describe("notation plugin install", func() { MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath, "--force"). - MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") + MatchContent("Succussefully installed plugin e2e-plugin, updated the version from 1.0.0 to 1.0.0\n") }) }) From 0145452d01cd6474577c6d73cfc8763c1d9e74c7 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 17 Nov 2023 11:58:48 +0800 Subject: [PATCH 39/83] updated based on spec Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index d9f96537f..8a184c904 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -54,8 +54,9 @@ func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { opts = &pluginInstallOpts{} } command := &cobra.Command{ - Use: "install [flags] <--file|--url> ", - Short: "Install plugin", + Use: "install [flags] <--file|--url> ", + Aliases: []string{"add"}, + Short: "Install plugin", Long: `Install a plugin Example - Install plugin from file system: @@ -97,7 +98,7 @@ Example - Install plugin from URL, SHA256 checksum is required: } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) command.Flags().BoolVar(&opts.isFile, "file", false, "install plugin from a file in file system") - command.Flags().BoolVar(&opts.isUrl, "url", false, " install plugin from a HTTPS URL") + command.Flags().BoolVar(&opts.isUrl, "url", false, "install plugin from an HTTPS URL") command.Flags().StringVar(&opts.inputCheckSum, "sha256sum", "", "must match SHA256 of the plugin source") command.Flags().BoolVar(&opts.force, "force", false, "force the installation of a plugin") command.MarkFlagsMutuallyExclusive("file", "url") @@ -297,12 +298,12 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st return err } if comp < 0 { - return fmt.Errorf("%s current version %s is higher than the installing version %s.\nTo view a list of installed plugins, use `notation plugin list`. If you want to install an old version, use \"--force\" to force the installation", pluginName, currentPluginVersion, pluginVersion) + return fmt.Errorf("%s. The installing version %s is lower than the existing plugin version %s.\nIt is not recommended to install an older version. Use \"--force\" to force the installation if you want to do so. To view a list of installed plugins, use `notation plugin list`", pluginName, pluginVersion, currentPluginVersion) } if comp == 0 { // if version is the same, no action is needed and no error is // returned - fmt.Printf("Plugin %s with version %s already exists.\nTo view a list of installed plugins, use `notation plugin list`.\n", pluginName, currentPluginVersion) + fmt.Printf("Plugin %s with version %s already exists.\nTo view a list of installed plugins, use `notation plugin list`\n", pluginName, currentPluginVersion) return nil } } From c8d9c5129071596a142b5e2d0c86544e2ec513a9 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 17 Nov 2023 12:03:10 +0800 Subject: [PATCH 40/83] fixed e2e Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 6fb44f81f..f78d4cadc 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -45,7 +45,7 @@ var _ = Describe("notation plugin install", func() { MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). - MatchContent("Plugin e2e-plugin with version 1.0.0 already exists.\nTo view a list of installed plugins, use `notation plugin list`.\n") + MatchContent("Plugin e2e-plugin with version 1.0.0 already exists.\nTo view a list of installed plugins, use `notation plugin list`\n") }) }) From 364d6806842bf1894a65cae0875fe4d1ad1608af Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 20 Nov 2023 10:45:15 +0800 Subject: [PATCH 41/83] updated based on spec Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 7 ++----- test/e2e/suite/plugin/install.go | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 8a184c904..9a51a4e5f 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -298,13 +298,10 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st return err } if comp < 0 { - return fmt.Errorf("%s. The installing version %s is lower than the existing plugin version %s.\nIt is not recommended to install an older version. Use \"--force\" to force the installation if you want to do so. To view a list of installed plugins, use `notation plugin list`", pluginName, pluginVersion, currentPluginVersion) + return fmt.Errorf("%s. The installing version %s is lower than the existing plugin version %s.\nIt is not recommended to install an older version. To force the installation, use the \"--force\" option", pluginName, pluginVersion, currentPluginVersion) } if comp == 0 { - // if version is the same, no action is needed and no error is - // returned - fmt.Printf("Plugin %s with version %s already exists.\nTo view a list of installed plugins, use `notation plugin list`\n", pluginName, currentPluginVersion) - return nil + return fmt.Errorf("%s with version %s already exists", pluginName, currentPluginVersion) } } } diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index f78d4cadc..9bcd67755 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -44,8 +44,8 @@ var _ = Describe("notation plugin install", func() { notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") - notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). - MatchContent("Plugin e2e-plugin with version 1.0.0 already exists.\nTo view a list of installed plugins, use `notation plugin list`\n") + notation.ExpectFailure().Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). + MatchContent("Error: failed to install the plugin: e2e-plugin with version 1.0.0 already exists\n") }) }) From c045bd55193ae35d908e8d4a10ae2e5486c25d8c Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 20 Nov 2023 10:49:02 +0800 Subject: [PATCH 42/83] updated based on spec Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 9bcd67755..1901bf967 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -45,7 +45,7 @@ var _ = Describe("notation plugin install", func() { MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") notation.ExpectFailure().Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). - MatchContent("Error: failed to install the plugin: e2e-plugin with version 1.0.0 already exists\n") + MatchErrContent("Error: failed to install the plugin: e2e-plugin with version 1.0.0 already exists\n") }) }) From 129daa04fbd2615a3026ca998c42b07ef3a0f6ca Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 20 Nov 2023 14:36:54 +0800 Subject: [PATCH 43/83] bump: bump up to go version 1.21 (#833) Signed-off-by: Patrick Zheng --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 298ad0948..9b43802eb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/notaryproject/notation -go 1.20 +go 1.21 require ( github.com/notaryproject/notation-core-go v1.0.1 From 84e60aa22c3169b51f8bcf05f1da2ab336b3ecb1 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 20 Nov 2023 15:01:15 +0800 Subject: [PATCH 44/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index bde71ca27..63d48cc99 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -44,10 +44,7 @@ type PluginSourceType int const ( // PluginSourceTypeFile means plugin source is file - PluginSourceTypeUnknown PluginSourceType = 1 + iota - - // PluginSourceTypeFile means plugin source is file - PluginSourceTypeFile + PluginSourceTypeFile PluginSourceType = 1 + iota // PluginSourceTypeURL means plugin source is URL PluginSourceTypeURL From 125b169811e8a7d0ae04d17d9dee8107a0880bdd Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 20 Nov 2023 15:25:49 +0800 Subject: [PATCH 45/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 9a51a4e5f..d9b8e0c4a 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -199,7 +199,7 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error // validate and get plugin name from file name pluginName, err := notationplugin.ExtractPluginNameFromFileName(f.Name) if err != nil { - logger.Infoln(err) + logger.Infof("processing file %s. File name not in format notation-{plugin-name}, skipped", f.Name) continue } fileInArchive, err := f.Open() From dfbbba4750512dff16dd71f803cb164ede326f32 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 20 Nov 2023 15:27:28 +0800 Subject: [PATCH 46/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index d9b8e0c4a..f1349763e 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -243,7 +243,7 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e // validate and get plugin name from file name pluginName, err := notationplugin.ExtractPluginNameFromFileName(header.Name) if err != nil { - logger.Infoln(err) + logger.Infof("processing file %s. File name not in format notation-{plugin-name}, skipped", header.Name) continue } return installPluginExecutable(ctx, header.Name, pluginName, tarReader, force) From 8cef5eba5ee12c88f72e21c73fc5b14fc0036b0b Mon Sep 17 00:00:00 2001 From: Feynman Zhou Date: Tue, 21 Nov 2023 07:59:21 +0800 Subject: [PATCH 47/83] doc: update plugin spec (#809) Signed-off-by: Feynman Zhou --- specs/commandline/plugin.md | 101 +++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 19 deletions(-) diff --git a/specs/commandline/plugin.md b/specs/commandline/plugin.md index be34979ef..9cfdea42e 100644 --- a/specs/commandline/plugin.md +++ b/specs/commandline/plugin.md @@ -2,7 +2,7 @@ ## Description -Use `notation plugin` to manage plugins. See notation [plugin documentation](https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md) for more details. The `notation plugin` command by itself performs no action. In order to operate on a plugin, one of the subcommands must be used. +Use `notation plugin` to manage plugins. See notation [plugin documentation](https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md) for more details. The `notation plugin` command by itself performs no action. In order to manage notation plugins, one of the subcommands must be used. ## Outline @@ -15,9 +15,9 @@ Usage: notation plugin [command] Available Commands: + install Install a plugin list List installed plugins - install Installs a plugin - remove Removes a plugin + uninstall Uninstall a plugin Flags: -h, --help help for plugin @@ -41,51 +41,114 @@ Aliases: ### notation plugin install ```text -Installs a plugin +Install a plugin Usage: - notation plugin install [flags] + notation plugin install [flags] <--file|--url> Flags: - -h, --help help for install - -f, --force force the installation of a plugin + -d, --debug debug mode + --file install plugin from a file in file system + --force force the installation of a plugin + -h, --help help for install + --sha256sum string must match SHA256 of the plugin source + --url install plugin from an HTTPS URL + -v, --verbose verbose mode Aliases: install, add ``` -### notation plugin remove +### notation plugin uninstall ```text -Removes a plugin +Uninstall a plugin Usage: - notation plugin remove [flags] + notation plugin uninstall [flags] Flags: -h, --help help for remove - + -y, --yes do not prompt for confirmation Aliases: - remove, rm, uninstall, delete + uninstall, remove, rm ``` ## Usage -### Install a plugin +## Install a plugin + +### Install a plugin from file system + +Install a Notation plugin from file system. Plugin file supports `.zip` and `.tar.gz` format. The checksum validation is optional for this case. ```shell -notation plugin install +$ notation plugin install --file +``` + +Upon successful execution, the plugin is copied to Notation's plugin directory. If the plugin directory does not exist, it will be created. The name and version of the installed plugin are displayed as follows. + +```console +Successfully installed plugin , version +``` + +If the entered plugin checksum digest doesn't match the published checksum, Notation will return an error message and will not start installation. + +```console +Error: failed to install the plugin: plugin checksum does not match user input. Expecting +``` + +If the plugin version is higher than the existing plugin, Notation will start installation and overwrite the existing plugin. + +```console +Successfully installed plugin , updated the version from to +``` + +If the plugin version is equal to the existing plugin, Notation will not start installation and return the following message. Users can use a flag `--force` to skip plugin version check and force the installation. + +```console +Error: failed to install the plugin: with version already exists. +``` + +If the plugin version is lower than the existing plugin, Notation will return an error message and will not start installation. Users can use a flag `--force` to skip plugin version check and force the installation. + +```console +Error: failed to install the plugin: . The installing plugin version is lower than the existing plugin version . +It is not recommended to install an older version. To force the installation, use the "--force" option. ``` +### Install a plugin from URL -Upon successful execution, the plugin is copied to plugins directory and name+version of plugin is displayed. If the plugin directory does not exist, it will be created. When an existing plugin is detected, the versions are compared and if the existing plugin is a lower version then it is replaced by the newer version. +Install a Notation plugin from a remote location and verify the plugin checksum. Notation only supports installing plugins from an HTTPS URL, which means that the URL must start with "https://". + +```shell +$ notation plugin install --sha256sum --url +``` ### Uninstall a plugin ```shell -notation plugin remove +notation plugin uninstall +``` + +Upon successful execution, the plugin is uninstalled from the plugin directory. + +```shell +Are you sure you want to uninstall plugin ""? [y/n] y +Successfully uninstalled +``` + +Uninstall the plugin without prompt for confirmation. + +```shell +notation plugin uninstall --yes ``` -Upon successful execution, the plugin is removed from the plugins directory. If the plugin is not found, an error is returned showing the syntax for the plugin list command to show the installed plugins. +If the plugin is not found, an error is returned showing the syntax for the plugin list command to show the installed plugins. + +```shell +Error: unable to find plugin . +To view a list of installed plugins, use "notation plugin list" +``` ### List installed plugins @@ -99,6 +162,6 @@ An example of output from `notation plugin list`: ```text NAME DESCRIPTION VERSION CAPABILITIES ERROR -azure-kv Sign artifacts with keys in Azure Key Vault v0.5.0-rc.1 [SIGNATURE_GENERATOR.RAW] -com.amazonaws.signer.notation.plugin AWS Signer plugin for Notation 1.0.290 [SIGNATURE_GENERATOR.ENVELOPE SIGNATURE_VERIFIER.TRUSTED_IDENTITY SIGNATURE_VERIFIER.REVOCATION_CHECK] +azure-kv Sign artifacts with keys in Azure Key Vault v1.0.0 Signature generation +com.amazonaws.signer.notation.plugin AWS Signer plugin for Notation 1.0.290 Signature envelope generation, Trusted Identity validation, Certificate chain revocation check ``` From 583ff98dec2e1111c7149d36d458d453444302e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Nov 2023 08:03:15 +0800 Subject: [PATCH 48/83] build(deps): Bump github.com/spf13/cobra from 1.7.0 to 1.8.0 (#823) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 9b43802eb..4b3bf06d6 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( 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.7.0 + github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 golang.org/x/mod v0.13.0 golang.org/x/term v0.13.0 diff --git a/go.sum b/go.sum index 65b112dc5..d30a5638e 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -33,8 +33,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 2624fe7ffe5bfcb39dbdb06249ef74108b7f2645 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 21 Nov 2023 13:57:32 +0800 Subject: [PATCH 49/83] update Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index f1349763e..1087a25f6 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -63,7 +63,7 @@ Example - Install plugin from file system: notation plugin install --file wabbit-plugin-v1.0.zip Example - Install plugin from file system with user input SHA256 checksum: - notation plugin install --file wabbit-plugin-v1.0.zip --checksum abcdef + notation plugin install --file wabbit-plugin-v1.0.zip --sha256sum 113062a462674a0e35cb5cad75a0bb2ea16e9537025531c0fd705018fcdbc17e Example - Install plugin from file system regardless if it's already installed: notation plugin install --file wabbit-plugin-v1.0.zip --force @@ -72,7 +72,7 @@ Example - Install plugin from file system with .tar.gz: notation plugin install --file wabbit-plugin-v1.0.tar.gz Example - Install plugin from URL, SHA256 checksum is required: - notation plugin install --url https://wabbit-networks.com/intaller/linux/amd64/wabbit-plugin-v1.0.tar.gz --checksum abcxyz + notation plugin install --url https://wabbit-networks.com/intaller/linux/amd64/wabbit-plugin-v1.0.tar.gz --sha256sum f8a75d9234db90069d9eb5660e5374820edf36d710bd063f4ef81e7063d3810b `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { From a272fdfc65d3c5268f4a566247fa13e107d32ced Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 23 Nov 2023 11:16:20 +0800 Subject: [PATCH 50/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 78 ++++----------------- cmd/notation/internal/plugin/plugin_test.go | 37 +--------- cmd/notation/plugin/install.go | 66 +++++++++-------- cmd/notation/registry.go | 17 +---- internal/osutil/file.go | 29 ++++++-- internal/osutil/file_test.go | 10 +++ internal/semver/semver.go | 42 +++++++++++ internal/semver/semver_test.go | 39 +++++++++++ internal/trace/transport.go | 17 +++++ test/e2e/suite/plugin/install.go | 39 ++++++++++- 10 files changed, 221 insertions(+), 153 deletions(-) create mode 100644 internal/semver/semver.go create mode 100644 internal/semver/semver_test.go diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 63d48cc99..d36e52752 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -18,20 +18,15 @@ import ( "fmt" "io" "net/http" - "os" "strings" "github.com/notaryproject/notation-go/dir" - "github.com/notaryproject/notation-go/log" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/proto" notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" "github.com/notaryproject/notation/internal/osutil" "github.com/notaryproject/notation/internal/trace" "github.com/notaryproject/notation/internal/version" - "github.com/opencontainers/go-digest" - "github.com/sirupsen/logrus" - "golang.org/x/mod/semver" "oras.land/oras-go/v2/registry/remote/auth" ) @@ -77,47 +72,11 @@ func GetPluginMetadata(ctx context.Context, pluginName, path string) (*proto.Get return plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) } -// ComparePluginVersion validates and compares two plugin semantic versions -func ComparePluginVersion(v, w string) (int, error) { - // semantic version strings must begin with a leading "v" - if !strings.HasPrefix(v, "v") { - v = "v" + v - } - if !semver.IsValid(v) { - return 0, fmt.Errorf("%s is not a valid semantic version", v) - } - if !strings.HasPrefix(w, "v") { - w = "v" + w - } - if !semver.IsValid(w) { - return 0, fmt.Errorf("%s is not a valid semantic version", w) - } - return semver.Compare(v, w), nil -} - -// ValidateCheckSum returns nil if SHA256 of file at path equals to checkSum. -func ValidateCheckSum(path string, checkSum string) error { - rc, err := os.Open(path) - if err != nil { - return err - } - defer rc.Close() - dgst, err := digest.FromReader(rc) - if err != nil { - return err - } - enc := dgst.Encoded() - if enc != strings.ToLower(checkSum) { - return fmt.Errorf("plugin checksum does not match user input. Expecting %s", checkSum) - } - return nil -} - // DownloadPluginFromURL downloads plugin file from url to a tmp directory -func DownloadPluginFromURL(ctx context.Context, url string, tmpFile *os.File) error { +func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Writer) error { // Get the data client := getClient(ctx) - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest("GET", pluginURL, nil) if err != nil { return err } @@ -128,14 +87,20 @@ func DownloadPluginFromURL(ctx context.Context, url string, tmpFile *os.File) er defer resp.Body.Close() // Check server response if resp.StatusCode != http.StatusOK { - return fmt.Errorf("bad status: %s", resp.Status) + return fmt.Errorf("https response bad status: %s", resp.Status) } // Write the body to file - lr := io.LimitReader(resp.Body, maxPluginSourceBytes) + lr := &io.LimitedReader{ + R: resp.Body, + N: maxPluginSourceBytes, + } _, err = io.Copy(tmpFile, lr) if err != nil { return err } + if lr.N == 0 { + return fmt.Errorf("https response reaches the %d MiB size limit", maxPluginSourceBytes) + } return nil } @@ -146,34 +111,17 @@ func getClient(ctx context.Context) *auth.Client { ClientID: "notation", } client.SetUserAgent("notation/" + version.GetVersion()) - setHttpDebugLog(ctx, client) + trace.SetHttpDebugLog(ctx, client) return client } -// setHttpDebugLog sets http debug logger -func setHttpDebugLog(ctx context.Context, authClient *auth.Client) { - if logrusLog, ok := log.GetLogger(ctx).(*logrus.Logger); ok && logrusLog.Level != logrus.DebugLevel { - return - } - if authClient.Client == nil { - authClient.Client = http.DefaultClient - } - if authClient.Client.Transport == nil { - authClient.Client.Transport = http.DefaultTransport - } - authClient.Client.Transport = trace.NewTransport(authClient.Client.Transport) -} - // ExtractPluginNameFromFileName checks if fileName is a valid plugin file name // and gets plugin name from it based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation func ExtractPluginNameFromFileName(fileName string) (string, error) { fname := osutil.FileNameWithoutExtension(fileName) - if !strings.HasPrefix(fname, proto.Prefix) { - return "", notationerrors.ErrorInvalidPluginFileName{Msg: fmt.Sprintf("invalid plugin file name. Plugin file name requires format notation-{plugin-name}, but got %s", fname)} - } - _, pluginName, found := strings.Cut(fname, "-") + pluginName, found := strings.CutPrefix(fname, proto.Prefix) if !found { - return "", notationerrors.ErrorInvalidPluginFileName{Msg: fmt.Sprintf("invalid plugin file name. Plugin file name requires format notation-{plugin-name}, but got %s", fname)} + return "", notationerrors.ErrorInvalidPluginFileName{Msg: fmt.Sprintf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got %s", fname)} } return pluginName, nil } diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go index d958d5746..7f6fcb6f1 100644 --- a/cmd/notation/internal/plugin/plugin_test.go +++ b/cmd/notation/internal/plugin/plugin_test.go @@ -27,39 +27,6 @@ func TestGetPluginMetadataIfExist(t *testing.T) { } } -func TestComparePluginVersion(t *testing.T) { - comp, err := ComparePluginVersion("v1.0.0", "v1.0.1") - if err != nil || comp >= 0 { - t.Fatal("expected nil err and negative comp") - } - - comp, err = ComparePluginVersion("1.0.0", "1.0.1") - if err != nil || comp >= 0 { - t.Fatal("expected nil err and negative comp") - } - - comp, err = ComparePluginVersion("1.0.1", "1.0.1") - if err != nil || comp != 0 { - t.Fatal("expected nil err and comp equal to 0") - } - - expectedErrMsg := "vabc is not a valid semantic version" - _, err = ComparePluginVersion("abc", "1.0.1") - if err == nil || err.Error() != expectedErrMsg { - t.Fatalf("expected err %s, got %s", expectedErrMsg, err) - } -} - -func TestValidateCheckSum(t *testing.T) { - expectedErrorMsg := "plugin checksum does not match user input. Expecting abcd123" - if err := ValidateCheckSum("./testdata/test", "abcd123"); err == nil || err.Error() != expectedErrorMsg { - t.Fatalf("expected err %s, got %v", expectedErrorMsg, err) - } - if err := ValidateCheckSum("./testdata/test", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); err != nil { - t.Fatalf("expected nil err, got %v", err) - } -} - func TestExtractPluginNameFromExecutableFileName(t *testing.T) { pluginName, err := ExtractPluginNameFromFileName("notation-my-plugin") if err != nil { @@ -78,13 +45,13 @@ func TestExtractPluginNameFromExecutableFileName(t *testing.T) { } _, err = ExtractPluginNameFromFileName("myPlugin") - expectedErrorMsg := "invalid plugin file name. Plugin file name requires format notation-{plugin-name}, but got myPlugin" + expectedErrorMsg := "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got myPlugin" if err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected %s, got %v", expectedErrorMsg, err) } _, err = ExtractPluginNameFromFileName("my-plugin") - expectedErrorMsg = "invalid plugin file name. Plugin file name requires format notation-{plugin-name}, but got my-plugin" + expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got my-plugin" if err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected %s, got %v", expectedErrorMsg, err) } diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 1087a25f6..e0f956672 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -32,18 +32,20 @@ import ( notationplugin "github.com/notaryproject/notation/cmd/notation/internal/plugin" "github.com/notaryproject/notation/internal/cmd" "github.com/notaryproject/notation/internal/osutil" + "github.com/notaryproject/notation/internal/semver" "github.com/spf13/cobra" ) const ( - notationPluginTmpDir = "notationPluginTmpDir" + notationPluginTmpDir = "notation-plugin" + notationPluginDownloadTmpFile = "notation-plugin-download" ) type pluginInstallOpts struct { cmd.LoggingFlagOpts pluginSourceType notationplugin.PluginSourceType pluginSource string - inputCheckSum string + inputChecksum string isFile bool isUrl bool force bool @@ -76,32 +78,33 @@ Example - Install plugin from URL, SHA256 checksum is required: `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return errors.New("missing plugin file path or URL") + if opts.isFile { + return errors.New("missing plugin file path") + } + if opts.isUrl { + return errors.New("missing plugin URL") + } + return errors.New("missing plugin source") } opts.pluginSource = args[0] return nil }, - PreRunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { if opts.isFile { opts.pluginSourceType = notationplugin.PluginSourceTypeFile - return nil - } - if opts.isUrl { + } else if opts.isUrl { opts.pluginSourceType = notationplugin.PluginSourceTypeURL - return nil } - return errors.New("must choose one and only one flag from [--file, --url]") - }, - RunE: func(cmd *cobra.Command, args []string) error { return installPlugin(cmd, opts) }, } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) command.Flags().BoolVar(&opts.isFile, "file", false, "install plugin from a file in file system") command.Flags().BoolVar(&opts.isUrl, "url", false, "install plugin from an HTTPS URL") - command.Flags().StringVar(&opts.inputCheckSum, "sha256sum", "", "must match SHA256 of the plugin source") + command.Flags().StringVar(&opts.inputChecksum, "sha256sum", "", "must match SHA256 of the plugin source, required when \"--url\" flag is set") command.Flags().BoolVar(&opts.force, "force", false, "force the installation of a plugin") command.MarkFlagsMutuallyExclusive("file", "url") + command.MarkFlagsOneRequired("file", "url") return command } @@ -111,19 +114,19 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { // core process switch opts.pluginSourceType { case notationplugin.PluginSourceTypeFile: - return installFromFileSystem(ctx, opts.pluginSource, opts.inputCheckSum, opts.force) + return installFromFileSystem(ctx, opts.pluginSource, opts.inputChecksum, opts.force) case notationplugin.PluginSourceTypeURL: - if opts.inputCheckSum == "" { + if opts.inputChecksum == "" { return errors.New("install from URL requires non-empty SHA256 checksum of the plugin source") } - url, err := url.Parse(opts.pluginSource) + pluginURL, err := url.Parse(opts.pluginSource) if err != nil { return fmt.Errorf("failed to install from URL: %v", err) } - if url.Scheme != "https" { + if pluginURL.Scheme != "https" { return fmt.Errorf("failed to install from URL: %q scheme is not HTTPS", opts.pluginSource) } - tmpFile, err := os.CreateTemp(".", "notationPluginDownloadTmp") + tmpFile, err := os.CreateTemp("", notationPluginDownloadTmpFile) if err != nil { return err } @@ -137,14 +140,14 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { if err != nil { return err } - return installFromFileSystem(ctx, downloadPath, opts.inputCheckSum, opts.force) + return installFromFileSystem(ctx, downloadPath, opts.inputChecksum, opts.force) default: return errors.New("failed to install the plugin: plugin source type is unknown") } } // installFromFileSystem install the plugin from file system -func installFromFileSystem(ctx context.Context, inputPath string, inputCheckSum string, force bool) error { +func installFromFileSystem(ctx context.Context, inputPath string, inputChecksum string, force bool) error { // sanity check inputFileStat, err := os.Stat(inputPath) if err != nil { @@ -154,8 +157,8 @@ func installFromFileSystem(ctx context.Context, inputPath string, inputCheckSum return fmt.Errorf("failed to install the plugin: %s is not a regular file", inputPath) } // checksum check - if inputCheckSum != "" { - if err := notationplugin.ValidateCheckSum(inputPath, inputCheckSum); err != nil { + if inputChecksum != "" { + if err := osutil.ValidateChecksum(inputPath, inputChecksum); err != nil { return fmt.Errorf("failed to install the plugin: %w", err) } } @@ -261,7 +264,7 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st return fmt.Errorf("on %s, plugin executable file %s cannot have the '.exe' extension", runtime.GOOS, fileName) } // extract to tmp dir - tmpDir, err := os.MkdirTemp(".", notationPluginTmpDir) + tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) if err != nil { return fmt.Errorf("failed to create notationPluginTmpDir: %w", err) } @@ -272,7 +275,7 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st return err } if _, err := io.Copy(pluginFile, fileReader); err != nil { - defer pluginFile.Close() + _ = pluginFile.Close() return err } if err := pluginFile.Close(); err != nil { @@ -287,20 +290,21 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st // check plugin existence and version var currentPluginVersion string currentPluginMetadata, err := notationplugin.GetPluginMetadataIfExist(ctx, pluginName) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - if err == nil { // plugin already exists + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return err + } + } else { // plugin already exists currentPluginVersion = currentPluginMetadata.Version if !force { - comp, err := notationplugin.ComparePluginVersion(pluginVersion, currentPluginVersion) + comp, err := semver.ComparePluginVersion(pluginVersion, currentPluginVersion) if err != nil { return err } - if comp < 0 { + switch { + case comp < 0: return fmt.Errorf("%s. The installing version %s is lower than the existing plugin version %s.\nIt is not recommended to install an older version. To force the installation, use the \"--force\" option", pluginName, pluginVersion, currentPluginVersion) - } - if comp == 0 { + case comp == 0: return fmt.Errorf("%s with version %s already exists", pluginName, currentPluginVersion) } } diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 7ef069f21..21a409f2b 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "net" - "net/http" "github.com/notaryproject/notation-go/log" notationregistry "github.com/notaryproject/notation-go/registry" @@ -28,7 +27,6 @@ import ( "github.com/notaryproject/notation/internal/version" "github.com/notaryproject/notation/pkg/configutil" credentials "github.com/oras-project/oras-credentials-go" - "github.com/sirupsen/logrus" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" @@ -122,19 +120,6 @@ func getRegistryLoginClient(ctx context.Context, opts *SecureFlagOpts, serverAdd return reg, nil } -func setHttpDebugLog(ctx context.Context, authClient *auth.Client) { - if logrusLog, ok := log.GetLogger(ctx).(*logrus.Logger); ok && logrusLog.Level != logrus.DebugLevel { - return - } - if authClient.Client == nil { - authClient.Client = http.DefaultClient - } - if authClient.Client.Transport == nil { - authClient.Client.Transport = http.DefaultTransport - } - authClient.Client.Transport = trace.NewTransport(authClient.Client.Transport) -} - // getAuthClient returns an *auth.Client and a bool indicating if the registry // is insecure. // @@ -162,7 +147,7 @@ func getAuthClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Refer ClientID: "notation", } authClient.SetUserAgent("notation/" + version.GetVersion()) - setHttpDebugLog(ctx, authClient) + trace.SetHttpDebugLog(ctx, authClient) if !withCredential { return authClient, insecureRegistry, nil } diff --git a/internal/osutil/file.go b/internal/osutil/file.go index 9f0bca006..150b0bf4f 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -14,6 +14,8 @@ package osutil import ( + "crypto/sha256" + "encoding/hex" "fmt" "io" "io/fs" @@ -104,12 +106,12 @@ func DetectFileType(path string) (string, error) { return "", err } defer rc.Close() - var header [512]byte - _, err = io.ReadFull(rc, header[:]) - if err != nil { + lr := io.LimitReader(rc, 512) + header := make([]byte, 512) + if _, err := lr.Read(header); err != nil { return "", err } - return http.DetectContentType(header[:]), nil + return http.DetectContentType(header), nil } // FileNameWithoutExtension returns the file name without extension. @@ -120,3 +122,22 @@ func FileNameWithoutExtension(inputName string) string { fileName := filepath.Base(inputName) return strings.TrimSuffix(fileName, filepath.Ext(fileName)) } + +// ValidateChecksum returns nil if SHA256 of file at path equals to checksum. +func ValidateChecksum(path string, checksum string) error { + rc, err := os.Open(path) + if err != nil { + return err + } + defer rc.Close() + sha256Hash := sha256.New() + if _, err := io.Copy(sha256Hash, rc); err != nil { + return err + } + sha256sum := sha256Hash.Sum(nil) + enc := strings.ToLower(hex.EncodeToString(sha256sum[:])) + if enc != strings.ToLower(checksum) { + return fmt.Errorf("plugin checksum does not match user input. Expecting %s", checksum) + } + return nil +} diff --git a/internal/osutil/file_test.go b/internal/osutil/file_test.go index 12ef64d5c..16e2317db 100644 --- a/internal/osutil/file_test.go +++ b/internal/osutil/file_test.go @@ -269,3 +269,13 @@ func TestFileNameWithoutExtension(t *testing.T) { t.Errorf("expected '%s', but got '%s'", expectedOutput, actualOutput) } } + +func TestValidateChecksum(t *testing.T) { + expectedErrorMsg := "plugin checksum does not match user input. Expecting abcd123" + if err := ValidateChecksum("./testdata/test", "abcd123"); err == nil || err.Error() != expectedErrorMsg { + t.Fatalf("expected err %s, got %v", expectedErrorMsg, err) + } + if err := ValidateChecksum("./testdata/test", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); err != nil { + t.Fatalf("expected nil err, got %v", err) + } +} diff --git a/internal/semver/semver.go b/internal/semver/semver.go new file mode 100644 index 000000000..d9477c4b8 --- /dev/null +++ b/internal/semver/semver.go @@ -0,0 +1,42 @@ +// 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 semver + +import ( + "fmt" + "strings" + + "golang.org/x/mod/semver" +) + +// ComparePluginVersion validates and compares two plugin semantic versions +func ComparePluginVersion(v, w string) (int, error) { + // golang.org/x/mod/semver requires semantic version strings must begin + // with a leading "v". Adding prefix "v" in case the input plugin version + // does not have it. + // Reference: https://pkg.go.dev/golang.org/x/mod/semver#pkg-overview + if !strings.HasPrefix(v, "v") { + v = "v" + v + } + if !semver.IsValid(v) { + return 0, fmt.Errorf("%s is not a valid semantic version", v) + } + if !strings.HasPrefix(w, "v") { + w = "v" + w + } + if !semver.IsValid(w) { + return 0, fmt.Errorf("%s is not a valid semantic version", w) + } + return semver.Compare(v, w), nil +} diff --git a/internal/semver/semver_test.go b/internal/semver/semver_test.go new file mode 100644 index 000000000..fd91dea4b --- /dev/null +++ b/internal/semver/semver_test.go @@ -0,0 +1,39 @@ +// 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 semver + +import "testing" + +func TestComparePluginVersion(t *testing.T) { + comp, err := ComparePluginVersion("v1.0.0", "v1.0.1") + if err != nil || comp >= 0 { + t.Fatal("expected nil err and negative comp") + } + + comp, err = ComparePluginVersion("1.0.0", "1.0.1") + if err != nil || comp >= 0 { + t.Fatal("expected nil err and negative comp") + } + + comp, err = ComparePluginVersion("1.0.1", "1.0.1") + if err != nil || comp != 0 { + t.Fatal("expected nil err and comp equal to 0") + } + + expectedErrMsg := "vabc is not a valid semantic version" + _, err = ComparePluginVersion("abc", "1.0.1") + if err == nil || err.Error() != expectedErrMsg { + t.Fatalf("expected err %s, got %s", expectedErrMsg, err) + } +} diff --git a/internal/trace/transport.go b/internal/trace/transport.go index e75d334d0..728d678a0 100644 --- a/internal/trace/transport.go +++ b/internal/trace/transport.go @@ -30,10 +30,13 @@ limitations under the License. package trace import ( + "context" "net/http" "strings" "github.com/notaryproject/notation-go/log" + "github.com/sirupsen/logrus" + "oras.land/oras-go/v2/registry/remote/auth" ) // Transport is an http.RoundTripper that keeps track of the in-flight @@ -82,3 +85,17 @@ func logHeader(header http.Header, e log.Logger) { e.Debugf(" Empty header") } } + +// SetHttpDebugLog sets up http debug log with logrus.Logger +func SetHttpDebugLog(ctx context.Context, authClient *auth.Client) { + if logrusLog, ok := log.GetLogger(ctx).(*logrus.Logger); ok && logrusLog.Level != logrus.DebugLevel { + return + } + if authClient.Client == nil { + authClient.Client = http.DefaultClient + } + if authClient.Client.Transport == nil { + authClient.Client.Transport = http.DefaultTransport + } + authClient.Client.Transport = NewTransport(authClient.Client.Transport) +} diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 1901bf967..b085c3057 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -21,10 +21,45 @@ import ( const ( PluginURL = "https://github.com/notaryproject/notation-action/raw/e2e-test-plugin/tests/plugin_binaries/notation-e2e-test-plugin_0.1.0_linux_amd64.tar.gz" - PluginCheckSum = "be8d035024d3a96afb4118af32f2e201f126c7254b02f7bcffb3e3149d744fd2" + PluginChecksum = "be8d035024d3a96afb4118af32f2e201f126c7254b02f7bcffb3e3149d744fd2" ) var _ = Describe("notation plugin install", func() { + It("with missing file or url flag", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure().Exec("plugin", "install", "."). + MatchErrContent("Error: at least one of the flags in the group [file url] is required\n") + }) + }) + + It("with both file and url flags are set", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure().Exec("plugin", "install", "--file", "--url", "."). + MatchErrContent("Error: if any flags in the group [file url] are set none of the others can be; [file url] were all set\n") + }) + }) + + It("with missing plugin source", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure().Exec("plugin", "install"). + MatchErrContent("Error: missing plugin source\n") + }) + }) + + It("with missing plugin file path", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure().Exec("plugin", "install", "--file"). + MatchErrContent("Error: missing plugin file path\n") + }) + }) + + It("with missing plugin URL", func() { + Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { + notation.ExpectFailure().Exec("plugin", "install", "--url"). + MatchErrContent("Error: missing plugin URL\n") + }) + }) + It("with valid plugin file path", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath, "-v"). @@ -61,7 +96,7 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.Exec("plugin", "install", "--url", PluginURL, "--sha256sum", PluginCheckSum). + notation.Exec("plugin", "install", "--url", PluginURL, "--sha256sum", PluginChecksum). MatchContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") }) }) From d3ff85a89d04196246618bd9e666442d38e3d561 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 23 Nov 2023 13:58:53 +0800 Subject: [PATCH 51/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 12 ++++---- cmd/notation/plugin/install.go | 40 +++++++++++++++----------- go.mod | 10 +++---- go.sum | 20 ++++++------- specs/commandline/plugin.md | 6 ++-- test/e2e/suite/plugin/install.go | 6 ++-- 6 files changed, 52 insertions(+), 42 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index d36e52752..fff8190f7 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -30,9 +30,11 @@ import ( "oras.land/oras-go/v2/registry/remote/auth" ) -// maxPluginSourceBytes specifies the limit on how many response -// bytes are allowed in the server's response to the download from URL request -var maxPluginSourceBytes int64 = 256 * 1024 * 1024 // 256 MiB +// MaxPluginSourceBytes specifies the limit on how many bytes are allowed in the +// server's response to the download from URL request. +// It also specifies the limit of a potentail plugin executable file in a +// .tar.gz or .zip file. +var MaxPluginSourceBytes int64 = 256 * 1024 * 1024 // 256 MiB // PluginSourceType is an enum for plugin source type PluginSourceType int @@ -92,14 +94,14 @@ func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Wri // Write the body to file lr := &io.LimitedReader{ R: resp.Body, - N: maxPluginSourceBytes, + N: MaxPluginSourceBytes, } _, err = io.Copy(tmpFile, lr) if err != nil { return err } if lr.N == 0 { - return fmt.Errorf("https response reaches the %d MiB size limit", maxPluginSourceBytes) + return fmt.Errorf("https response reaches the %d MiB size limit", MaxPluginSourceBytes) } return nil } diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index e0f956672..612d94dff 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -102,7 +102,7 @@ Example - Install plugin from URL, SHA256 checksum is required: command.Flags().BoolVar(&opts.isFile, "file", false, "install plugin from a file in file system") command.Flags().BoolVar(&opts.isUrl, "url", false, "install plugin from an HTTPS URL") command.Flags().StringVar(&opts.inputChecksum, "sha256sum", "", "must match SHA256 of the plugin source, required when \"--url\" flag is set") - command.Flags().BoolVar(&opts.force, "force", false, "force the installation of a plugin") + command.Flags().BoolVar(&opts.force, "force", false, "force the installation of the plugin") command.MarkFlagsMutuallyExclusive("file", "url") command.MarkFlagsOneRequired("file", "url") return command @@ -121,10 +121,10 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { } pluginURL, err := url.Parse(opts.pluginSource) if err != nil { - return fmt.Errorf("failed to install from URL: %v", err) + return fmt.Errorf("the plugin download failed: %v", err) } if pluginURL.Scheme != "https" { - return fmt.Errorf("failed to install from URL: %q scheme is not HTTPS", opts.pluginSource) + return fmt.Errorf("the plugin download failed: only the HTTPS scheme is supported, but got %s", pluginURL.Scheme) } tmpFile, err := os.CreateTemp("", notationPluginDownloadTmpFile) if err != nil { @@ -142,7 +142,7 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { } return installFromFileSystem(ctx, downloadPath, opts.inputChecksum, opts.force) default: - return errors.New("failed to install the plugin: plugin source type is unknown") + return errors.New("plugin installation failed: unknown plugin source type") } } @@ -151,36 +151,36 @@ func installFromFileSystem(ctx context.Context, inputPath string, inputChecksum // sanity check inputFileStat, err := os.Stat(inputPath) if err != nil { - return fmt.Errorf("failed to install the plugin: %w", err) + return fmt.Errorf("plugin installation failed: %w", err) } if !inputFileStat.Mode().IsRegular() { - return fmt.Errorf("failed to install the plugin: %s is not a regular file", inputPath) + return fmt.Errorf("plugin installation failed: %s is not a valid file", inputPath) } // checksum check if inputChecksum != "" { if err := osutil.ValidateChecksum(inputPath, inputChecksum); err != nil { - return fmt.Errorf("failed to install the plugin: %w", err) + return fmt.Errorf("plugin installation failed: %w", err) } } // install the plugin based on file type fileType, err := osutil.DetectFileType(inputPath) if err != nil { - return fmt.Errorf("failed to install the plugin: %w", err) + return fmt.Errorf("plugin installation failed: %w", err) } switch fileType { case notationplugin.MediaTypeZip: if err := installPluginFromZip(ctx, inputPath, force); err != nil { - return fmt.Errorf("failed to install the plugin: %w", err) + return fmt.Errorf("plugin installation failed: %w", err) } return nil case notationplugin.MediaTypeGzip: // when file is gzip, require to be tar if err := installPluginFromTarGz(ctx, inputPath, force); err != nil { - return fmt.Errorf("failed to install the plugin: %w", err) + return fmt.Errorf("plugin installation failed: %w", err) } return nil default: - return errors.New("failed to install the plugin: invalid file format. Only support .tar.gz and .zip") + return errors.New("plugin installation failed: invalid file format. Only .tar.gz and .zip formats are supported") } } @@ -202,7 +202,7 @@ func installPluginFromZip(ctx context.Context, zipPath string, force bool) error // validate and get plugin name from file name pluginName, err := notationplugin.ExtractPluginNameFromFileName(f.Name) if err != nil { - logger.Infof("processing file %s. File name not in format notation-{plugin-name}, skipped", f.Name) + logger.Debugf("processing file %s. File name not in format notation-{plugin-name}, skipped", f.Name) continue } fileInArchive, err := f.Open() @@ -270,14 +270,22 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st } defer os.RemoveAll(tmpDir) tmpFilePath := filepath.Join(tmpDir, fileName) - pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700) + pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE, 0700) if err != nil { return err } - if _, err := io.Copy(pluginFile, fileReader); err != nil { + lr := &io.LimitedReader{ + R: fileReader, + N: notationplugin.MaxPluginSourceBytes, + } + if _, err := io.Copy(pluginFile, lr); err != nil || lr.N == 0 { _ = pluginFile.Close() - return err + if err != nil { + return err + } + return fmt.Errorf("plugin executable file reaches the %d MiB size limit", notationplugin.MaxPluginSourceBytes) } + if err := pluginFile.Close(); err != nil { return err } @@ -305,7 +313,7 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st case comp < 0: return fmt.Errorf("%s. The installing version %s is lower than the existing plugin version %s.\nIt is not recommended to install an older version. To force the installation, use the \"--force\" option", pluginName, pluginVersion, currentPluginVersion) case comp == 0: - return fmt.Errorf("%s with version %s already exists", pluginName, currentPluginVersion) + return fmt.Errorf("plugin %s with version %s already exists", pluginName, currentPluginVersion) } } } diff --git a/go.mod b/go.mod index 4b3bf06d6..30d5850bb 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,15 @@ 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/mod v0.13.0 - golang.org/x/term v0.13.0 + golang.org/x/mod v0.14.0 + golang.org/x/term v0.14.0 oras.land/oras-go/v2 v2.3.1 ) @@ -26,7 +26,7 @@ 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/crypto v0.15.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/specs/commandline/plugin.md b/specs/commandline/plugin.md index 9cfdea42e..5de10f67b 100644 --- a/specs/commandline/plugin.md +++ b/specs/commandline/plugin.md @@ -95,7 +95,7 @@ Successfully installed plugin , version If the entered plugin checksum digest doesn't match the published checksum, Notation will return an error message and will not start installation. ```console -Error: failed to install the plugin: plugin checksum does not match user input. Expecting +Error: plugin installation failed: plugin checksum does not match user input. Expecting ``` If the plugin version is higher than the existing plugin, Notation will start installation and overwrite the existing plugin. @@ -107,13 +107,13 @@ Successfully installed plugin , updated the version from to If the plugin version is equal to the existing plugin, Notation will not start installation and return the following message. Users can use a flag `--force` to skip plugin version check and force the installation. ```console -Error: failed to install the plugin: with version already exists. +Error: plugin installation failed: plugin with version already exists. ``` If the plugin version is lower than the existing plugin, Notation will return an error message and will not start installation. Users can use a flag `--force` to skip plugin version check and force the installation. ```console -Error: failed to install the plugin: . The installing plugin version is lower than the existing plugin version . +Error: plugin installation failed: . The installing plugin version is lower than the existing plugin version . It is not recommended to install an older version. To force the installation, use the "--force" option. ``` ### Install a plugin from URL diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index b085c3057..75b2136a4 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -70,7 +70,7 @@ var _ = Describe("notation plugin install", func() { It("with invalid plugin file type", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", "--file", NotationE2EPluginPath). - MatchErrContent("Error: failed to install the plugin: invalid file format. Only support .tar.gz and .zip\n") + MatchErrContent("Error: plugin installation failed: invalid file format. Only .tar.gz and .zip formats are supported\n") }) }) @@ -80,7 +80,7 @@ var _ = Describe("notation plugin install", func() { MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") notation.ExpectFailure().Exec("plugin", "install", "--file", NotationE2EPluginTarGzPath). - MatchErrContent("Error: failed to install the plugin: e2e-plugin with version 1.0.0 already exists\n") + MatchErrContent("Error: plugin installation failed: plugin e2e-plugin with version 1.0.0 already exists\n") }) }) @@ -111,7 +111,7 @@ var _ = Describe("notation plugin install", func() { It("with invalid plugin URL scheme", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", "--url", "http://invalid", "--sha256sum", "abcd"). - MatchErrContent("Error: failed to install from URL: \"http://invalid\" scheme is not HTTPS\n") + MatchErrContent("Error: the plugin download failed: only the HTTPS scheme is supported, but got \"http\"\n") }) }) From dbf5adb357b9682064f4c758f1b29f51f1f02a33 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 23 Nov 2023 14:10:25 +0800 Subject: [PATCH 52/83] fixing unit tests Signed-off-by: Patrick Zheng --- {cmd/notation/internal/plugin => internal/osutil}/testdata/test | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {cmd/notation/internal/plugin => internal/osutil}/testdata/test (100%) diff --git a/cmd/notation/internal/plugin/testdata/test b/internal/osutil/testdata/test similarity index 100% rename from cmd/notation/internal/plugin/testdata/test rename to internal/osutil/testdata/test From 5031090b6a291910415563463bc69a097cc11d8d Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 23 Nov 2023 14:15:14 +0800 Subject: [PATCH 53/83] fixing e2e tests Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 75b2136a4..9e9bc011c 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -111,7 +111,7 @@ var _ = Describe("notation plugin install", func() { It("with invalid plugin URL scheme", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", "--url", "http://invalid", "--sha256sum", "abcd"). - MatchErrContent("Error: the plugin download failed: only the HTTPS scheme is supported, but got \"http\"\n") + MatchErrContent("Error: the plugin download failed: only the HTTPS scheme is supported, but got http\n") }) }) From b2b66c4c577586490ad4e7690803aba05b307be0 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 24 Nov 2023 18:14:12 +0800 Subject: [PATCH 54/83] refactored the code Signed-off-by: Patrick Zheng --- cmd/notation/internal/errors/errors.go | 13 -- cmd/notation/internal/plugin/plugin.go | 53 +------ cmd/notation/internal/plugin/plugin_test.go | 58 -------- cmd/notation/plugin/install.go | 147 +++++++++----------- cmd/notation/registry.go | 9 +- go.mod | 4 +- go.sum | 4 +- internal/auth/client.go | 33 +++++ internal/osutil/file.go | 9 -- internal/osutil/file_test.go | 12 +- internal/semver/semver.go | 42 ------ internal/semver/semver_test.go | 39 ------ 12 files changed, 108 insertions(+), 315 deletions(-) delete mode 100644 cmd/notation/internal/plugin/plugin_test.go create mode 100644 internal/auth/client.go delete mode 100644 internal/semver/semver.go delete mode 100644 internal/semver/semver_test.go diff --git a/cmd/notation/internal/errors/errors.go b/cmd/notation/internal/errors/errors.go index 599b9c7a5..bb06e6f6f 100644 --- a/cmd/notation/internal/errors/errors.go +++ b/cmd/notation/internal/errors/errors.go @@ -50,16 +50,3 @@ type ErrorExceedMaxSignatures struct { func (e ErrorExceedMaxSignatures) Error() string { return fmt.Sprintf("exceeded configured limit of max signatures %d to examine", e.MaxSignatures) } - -// ErrorInvalidPluginFileName is used when a file name is not a valid plugin -// file name following the spec https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation. -type ErrorInvalidPluginFileName struct { - Msg string -} - -func (e ErrorInvalidPluginFileName) Error() string { - if e.Msg != "" { - return e.Msg - } - return "invalid plugin file name" -} diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index fff8190f7..ae94352c4 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -18,16 +18,8 @@ import ( "fmt" "io" "net/http" - "strings" - "github.com/notaryproject/notation-go/dir" - "github.com/notaryproject/notation-go/plugin" - "github.com/notaryproject/notation-go/plugin/proto" - notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors" - "github.com/notaryproject/notation/internal/osutil" - "github.com/notaryproject/notation/internal/trace" - "github.com/notaryproject/notation/internal/version" - "oras.land/oras-go/v2/registry/remote/auth" + notationauth "github.com/notaryproject/notation/internal/auth" ) // MaxPluginSourceBytes specifies the limit on how many bytes are allowed in the @@ -55,29 +47,10 @@ const ( MediaTypeGzip = "application/x-gzip" ) -// GetPluginMetadataIfExist returns plugin's metadata if it exists in Notation -func GetPluginMetadataIfExist(ctx context.Context, pluginName string) (*proto.GetMetadataResponse, error) { - mgr := plugin.NewCLIManager(dir.PluginFS()) - plugin, err := mgr.Get(ctx, pluginName) - if err != nil { - return nil, err - } - return plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) -} - -// GetPluginMetadata returns plugin's metadata given plugin path -func GetPluginMetadata(ctx context.Context, pluginName, path string) (*proto.GetMetadataResponse, error) { - plugin, err := plugin.NewCLIPlugin(ctx, pluginName, path) - if err != nil { - return nil, err - } - return plugin.GetMetadata(ctx, &proto.GetMetadataRequest{}) -} - // DownloadPluginFromURL downloads plugin file from url to a tmp directory func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Writer) error { // Get the data - client := getClient(ctx) + client := notationauth.NewAuthClient(ctx) req, err := http.NewRequest("GET", pluginURL, nil) if err != nil { return err @@ -105,25 +78,3 @@ func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Wri } return nil } - -// getClient returns an *auth.Client -func getClient(ctx context.Context) *auth.Client { - client := &auth.Client{ - Cache: auth.NewCache(), - ClientID: "notation", - } - client.SetUserAgent("notation/" + version.GetVersion()) - trace.SetHttpDebugLog(ctx, client) - return client -} - -// ExtractPluginNameFromFileName checks if fileName is a valid plugin file name -// and gets plugin name from it based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation -func ExtractPluginNameFromFileName(fileName string) (string, error) { - fname := osutil.FileNameWithoutExtension(fileName) - pluginName, found := strings.CutPrefix(fname, proto.Prefix) - if !found { - return "", notationerrors.ErrorInvalidPluginFileName{Msg: fmt.Sprintf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got %s", fname)} - } - return pluginName, nil -} diff --git a/cmd/notation/internal/plugin/plugin_test.go b/cmd/notation/internal/plugin/plugin_test.go deleted file mode 100644 index 7f6fcb6f1..000000000 --- a/cmd/notation/internal/plugin/plugin_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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" - "os" - "testing" -) - -func TestGetPluginMetadataIfExist(t *testing.T) { - _, err := GetPluginMetadataIfExist(context.Background(), "non-exist-plugin") - if !errors.Is(err, os.ErrNotExist) { - t.Fatalf("expected os.ErrNotExist err, got: %v", err) - } -} - -func TestExtractPluginNameFromExecutableFileName(t *testing.T) { - pluginName, err := ExtractPluginNameFromFileName("notation-my-plugin") - if err != nil { - t.Fatalf("expected nil err, got %v", err) - } - if pluginName != "my-plugin" { - t.Fatalf("expected plugin name my-plugin, but got %s", pluginName) - } - - pluginName, err = ExtractPluginNameFromFileName("notation-my-plugin.exe") - if err != nil { - t.Fatalf("expected nil err, got %v", err) - } - if pluginName != "my-plugin" { - t.Fatalf("expected plugin name my-plugin, but got %s", pluginName) - } - - _, err = ExtractPluginNameFromFileName("myPlugin") - expectedErrorMsg := "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got myPlugin" - if err == nil || err.Error() != expectedErrorMsg { - t.Fatalf("expected %s, got %v", expectedErrorMsg, err) - } - - _, err = ExtractPluginNameFromFileName("my-plugin") - expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got my-plugin" - if err == nil || err.Error() != expectedErrorMsg { - t.Fatalf("expected %s, got %v", expectedErrorMsg, err) - } -} diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 612d94dff..5672e0c0d 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -21,18 +21,18 @@ import ( "errors" "fmt" "io" + "io/fs" "net/url" "os" "path/filepath" - "runtime" "strings" "github.com/notaryproject/notation-go/dir" "github.com/notaryproject/notation-go/log" + "github.com/notaryproject/notation-go/plugin" notationplugin "github.com/notaryproject/notation/cmd/notation/internal/plugin" "github.com/notaryproject/notation/internal/cmd" "github.com/notaryproject/notation/internal/osutil" - "github.com/notaryproject/notation/internal/semver" "github.com/spf13/cobra" ) @@ -51,6 +51,8 @@ type pluginInstallOpts struct { force bool } +var ErrNoPluginExecutableFileWasFound = errors.New("no plugin executable file was found") + func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { if opts == nil { opts = &pluginInstallOpts{} @@ -169,7 +171,14 @@ func installFromFileSystem(ctx context.Context, inputPath string, inputChecksum } switch fileType { case notationplugin.MediaTypeZip: - if err := installPluginFromZip(ctx, inputPath, force); err != nil { + rc, err := zip.OpenReader(inputPath) + if err != nil { + return fmt.Errorf("plugin installation failed: %w", err) + } + if err := installPluginFromFS(ctx, rc, force); err != nil { + if errors.Is(err, ErrNoPluginExecutableFileWasFound) { + return fmt.Errorf("plugin installation failed: no valid plugin file was found in %s. Plugin file name must in format notation-{plugin-name}", inputPath) + } return fmt.Errorf("plugin installation failed: %w", err) } return nil @@ -184,35 +193,51 @@ func installFromFileSystem(ctx context.Context, inputPath string, inputChecksum } } -// installPluginFromZip extracts a plugin zip file, validates and -// installs the plugin -func installPluginFromZip(ctx context.Context, zipPath string, force bool) error { +// installPluginFromFS extracts , validates and installs the plugin from a fs.FS +func installPluginFromFS(ctx context.Context, pluginFs fs.FS, force bool) error { + // set up logger logger := log.GetLogger(ctx) - archive, err := zip.OpenReader(zipPath) - if err != nil { - return err - } - defer archive.Close() - // require one and only one file with name in the format - // `notation-{plugin-name}` - for _, f := range archive.File { - if !f.Mode().IsRegular() || strings.Contains(f.Name, "..") { - continue + root := "." + var success bool + if err := fs.WalkDir(pluginFs, root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + fName := d.Name() + if d.IsDir() && fName != root { // skip any dir in the fs + return fs.SkipDir } - // validate and get plugin name from file name - pluginName, err := notationplugin.ExtractPluginNameFromFileName(f.Name) + info, err := d.Info() if err != nil { - logger.Debugf("processing file %s. File name not in format notation-{plugin-name}, skipped", f.Name) - continue + return err + } + if !info.Mode().IsRegular() || strings.Contains(fName, "..") { + return nil + } + // validate the file name against the notation-{plugin-name} format + logger.Debugf("Processing file %s...", fName) + _, err = plugin.ExtractPluginNameFromFileName(fName) + if err != nil { + logger.Debugf("File name %s is not in format notation-{plugin-name}, skipped", fName) + return nil } - fileInArchive, err := f.Open() + rc, err := pluginFs.Open(path) if err != nil { return err } - defer fileInArchive.Close() - return installPluginExecutable(ctx, f.Name, pluginName, fileInArchive, force) + defer rc.Close() + if err := installPluginExecutable(ctx, fName, rc, force); err != nil { + return err + } + success = true + return fs.SkipAll + }); err != nil { + return err + } + if !success { + return ErrNoPluginExecutableFileWasFound } - return fmt.Errorf("no valid plugin file was found in %s. Plugin file name must in format notation-{plugin-name}", zipPath) + return nil } // installPluginFromTarGz extracts and untar a plugin tar.gz file, validates and @@ -243,27 +268,22 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e if !header.FileInfo().Mode().IsRegular() || strings.Contains(header.Name, "..") { continue } - // validate and get plugin name from file name - pluginName, err := notationplugin.ExtractPluginNameFromFileName(header.Name) + // validate the file name against the notation-{plugin-name} format + fName := filepath.Base(header.Name) + logger.Debugf("Processing file %s...", fName) + _, err = plugin.ExtractPluginNameFromFileName(fName) if err != nil { - logger.Infof("processing file %s. File name not in format notation-{plugin-name}, skipped", header.Name) + logger.Infof("File name %s is not in format notation-{plugin-name}, skipped", fName) continue } - return installPluginExecutable(ctx, header.Name, pluginName, tarReader, force) + return installPluginExecutable(ctx, fName, tarReader, force) } return fmt.Errorf("no valid plugin file was found in %s. Plugin file name must in format notation-{plugin-name}", tarGzPath) } -// installPluginExecutable extracts, validates, and installs a plugin file -func installPluginExecutable(ctx context.Context, fileName string, pluginName string, fileReader io.Reader, force bool) error { - // sanity check - if runtime.GOOS == "windows" && filepath.Ext(fileName) != ".exe" { - return fmt.Errorf("on Windows, plugin executable file %s is missing the '.exe' extension", fileName) - } - if runtime.GOOS != "windows" && filepath.Ext(fileName) == ".exe" { - return fmt.Errorf("on %s, plugin executable file %s cannot have the '.exe' extension", runtime.GOOS, fileName) - } - // extract to tmp dir +// installPluginExecutable extracts, validates, and installs a plugin executable +// file +func installPluginExecutable(ctx context.Context, fileName string, fileReader io.Reader, force bool) error { tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) if err != nil { return fmt.Errorf("failed to create notationPluginTmpDir: %w", err) @@ -285,57 +305,22 @@ func installPluginExecutable(ctx context.Context, fileName string, pluginName st } return fmt.Errorf("plugin executable file reaches the %d MiB size limit", notationplugin.MaxPluginSourceBytes) } - if err := pluginFile.Close(); err != nil { return err } - // get plugin metadata - pluginMetadata, err := notationplugin.GetPluginMetadata(ctx, pluginName, tmpFilePath) - if err != nil { - return err - } - pluginVersion := pluginMetadata.Version - // check plugin existence and version - var currentPluginVersion string - currentPluginMetadata, err := notationplugin.GetPluginMetadataIfExist(ctx, pluginName) + // core process + mgr := plugin.NewCLIManager(dir.PluginFS()) + existingPluginMetadata, newPluginMetadata, err := mgr.Install(ctx, tmpFilePath, force) if err != nil { - if !errors.Is(err, os.ErrNotExist) { - return err + if errors.Is(err, &plugin.ErrInstallLowerVersion{}) { + return fmt.Errorf("%s. %w.\nIt is not recommended to install an older version. To force the installation, use the \"--force\" option", newPluginMetadata.Name, err) } - } else { // plugin already exists - currentPluginVersion = currentPluginMetadata.Version - if !force { - comp, err := semver.ComparePluginVersion(pluginVersion, currentPluginVersion) - if err != nil { - return err - } - switch { - case comp < 0: - return fmt.Errorf("%s. The installing version %s is lower than the existing plugin version %s.\nIt is not recommended to install an older version. To force the installation, use the \"--force\" option", pluginName, pluginVersion, currentPluginVersion) - case comp == 0: - return fmt.Errorf("plugin %s with version %s already exists", pluginName, currentPluginVersion) - } - } - } - // install plugin - pluginPath, err := dir.PluginFS().SysPath(pluginName) - if err != nil { - return err - } - _, err = osutil.CopyToDir(tmpFilePath, pluginPath) - if err != nil { - return err - } - // plugin is always executable - pluginFilePath := filepath.Join(pluginPath, fileName) - err = os.Chmod(pluginFilePath, 0700) - if err != nil { return err } - if currentPluginVersion != "" { - fmt.Printf("Succussefully installed plugin %s, updated the version from %s to %s\n", pluginName, currentPluginVersion, pluginVersion) + if existingPluginMetadata != nil { + fmt.Printf("Succussefully installed plugin %s, updated the version from %s to %s\n", newPluginMetadata.Name, existingPluginMetadata.Version, newPluginMetadata.Version) } else { - fmt.Printf("Succussefully installed plugin %s, version %s\n", pluginName, pluginVersion) + fmt.Printf("Succussefully installed plugin %s, version %s\n", newPluginMetadata.Name, newPluginMetadata.Version) } return nil } diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 21a409f2b..9cb06943e 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -23,8 +23,6 @@ import ( notationregistry "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation/cmd/notation/internal/experimental" notationauth "github.com/notaryproject/notation/internal/auth" - "github.com/notaryproject/notation/internal/trace" - "github.com/notaryproject/notation/internal/version" "github.com/notaryproject/notation/pkg/configutil" credentials "github.com/oras-project/oras-credentials-go" "oras.land/oras-go/v2/registry" @@ -142,12 +140,7 @@ func getAuthClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Refer } // build authClient - authClient := &auth.Client{ - Cache: auth.NewCache(), - ClientID: "notation", - } - authClient.SetUserAgent("notation/" + version.GetVersion()) - trace.SetHttpDebugLog(ctx, authClient) + authClient := notationauth.NewAuthClient(ctx) if !withCredential { return authClient, insecureRegistry, nil } diff --git a/go.mod b/go.mod index 30d5850bb..f51f4ed62 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - golang.org/x/mod v0.14.0 golang.org/x/term v0.14.0 oras.land/oras-go/v2 v2.3.1 ) @@ -27,6 +26,9 @@ require ( github.com/veraison/go-cose v1.1.0 // indirect github.com/x448/float16 v0.8.4 // 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.14.0 // indirect ) + +replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231124094040-24aef8a46c1e diff --git a/go.sum b/go.sum index 4f1f59a82..b1ef0441c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/Two-Hearts/notation-go v0.0.0-20231124094040-24aef8a46c1e h1:PTr+6mKkHYwLajLtlD4YhsGm7WONPQ7AXshqHa03XR8= +github.com/Two-Hearts/notation-go v0.0.0-20231124094040-24aef8a46c1e/go.mod h1:tSCFsAdKAtB7AfKS/BaUf8AXzASA+9TEokMDEDutqPM= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -20,8 +22,6 @@ 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.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= diff --git a/internal/auth/client.go b/internal/auth/client.go new file mode 100644 index 000000000..410bc8e93 --- /dev/null +++ b/internal/auth/client.go @@ -0,0 +1,33 @@ +// 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 auth + +import ( + "context" + + "github.com/notaryproject/notation/internal/trace" + "github.com/notaryproject/notation/internal/version" + "oras.land/oras-go/v2/registry/remote/auth" +) + +// NewAuthClient returns an *auth.Client +func NewAuthClient(ctx context.Context) *auth.Client { + client := &auth.Client{ + Cache: auth.NewCache(), + ClientID: "notation", + } + client.SetUserAgent("notation/" + version.GetVersion()) + trace.SetHttpDebugLog(ctx, client) + return client +} diff --git a/internal/osutil/file.go b/internal/osutil/file.go index 150b0bf4f..4c7593454 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -114,15 +114,6 @@ func DetectFileType(path string) (string, error) { return http.DetectContentType(header), nil } -// FileNameWithoutExtension returns the file name without extension. -// For example, -// when input is xyz.exe, output is xyz -// when input is xyz.tar.gz, output is xyz.tar -func FileNameWithoutExtension(inputName string) string { - fileName := filepath.Base(inputName) - return strings.TrimSuffix(fileName, filepath.Ext(fileName)) -} - // ValidateChecksum returns nil if SHA256 of file at path equals to checksum. func ValidateChecksum(path string, checksum string) error { rc, err := os.Open(path) diff --git a/internal/osutil/file_test.go b/internal/osutil/file_test.go index 16e2317db..cc8218864 100644 --- a/internal/osutil/file_test.go +++ b/internal/osutil/file_test.go @@ -15,7 +15,6 @@ package osutil import ( "bytes" - "io/ioutil" "os" "path/filepath" "runtime" @@ -23,7 +22,7 @@ import ( ) func validFileContent(t *testing.T, filename string, content []byte) { - b, err := ioutil.ReadFile(filename) + b, err := os.ReadFile(filename) if err != nil { t.Fatal(err) } @@ -261,15 +260,6 @@ func TestCopyToDir(t *testing.T) { }) } -func TestFileNameWithoutExtension(t *testing.T) { - input := "testfile.tar.gz" - expectedOutput := "testfile.tar" - actualOutput := FileNameWithoutExtension(input) - if actualOutput != expectedOutput { - t.Errorf("expected '%s', but got '%s'", expectedOutput, actualOutput) - } -} - func TestValidateChecksum(t *testing.T) { expectedErrorMsg := "plugin checksum does not match user input. Expecting abcd123" if err := ValidateChecksum("./testdata/test", "abcd123"); err == nil || err.Error() != expectedErrorMsg { diff --git a/internal/semver/semver.go b/internal/semver/semver.go deleted file mode 100644 index d9477c4b8..000000000 --- a/internal/semver/semver.go +++ /dev/null @@ -1,42 +0,0 @@ -// 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 semver - -import ( - "fmt" - "strings" - - "golang.org/x/mod/semver" -) - -// ComparePluginVersion validates and compares two plugin semantic versions -func ComparePluginVersion(v, w string) (int, error) { - // golang.org/x/mod/semver requires semantic version strings must begin - // with a leading "v". Adding prefix "v" in case the input plugin version - // does not have it. - // Reference: https://pkg.go.dev/golang.org/x/mod/semver#pkg-overview - if !strings.HasPrefix(v, "v") { - v = "v" + v - } - if !semver.IsValid(v) { - return 0, fmt.Errorf("%s is not a valid semantic version", v) - } - if !strings.HasPrefix(w, "v") { - w = "v" + w - } - if !semver.IsValid(w) { - return 0, fmt.Errorf("%s is not a valid semantic version", w) - } - return semver.Compare(v, w), nil -} diff --git a/internal/semver/semver_test.go b/internal/semver/semver_test.go deleted file mode 100644 index fd91dea4b..000000000 --- a/internal/semver/semver_test.go +++ /dev/null @@ -1,39 +0,0 @@ -// 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 semver - -import "testing" - -func TestComparePluginVersion(t *testing.T) { - comp, err := ComparePluginVersion("v1.0.0", "v1.0.1") - if err != nil || comp >= 0 { - t.Fatal("expected nil err and negative comp") - } - - comp, err = ComparePluginVersion("1.0.0", "1.0.1") - if err != nil || comp >= 0 { - t.Fatal("expected nil err and negative comp") - } - - comp, err = ComparePluginVersion("1.0.1", "1.0.1") - if err != nil || comp != 0 { - t.Fatal("expected nil err and comp equal to 0") - } - - expectedErrMsg := "vabc is not a valid semantic version" - _, err = ComparePluginVersion("abc", "1.0.1") - if err == nil || err.Error() != expectedErrMsg { - t.Fatalf("expected err %s, got %s", expectedErrMsg, err) - } -} From 029317b44e1700852b488daf46fe528f55dc18af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 03:16:15 +0000 Subject: [PATCH 55/83] build(deps): Bump github/codeql-action from 2.22.5 to 2.22.7 (#835) --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 30bf2477f..e37b2c66f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -49,8 +49,8 @@ jobs: go-version: ${{ matrix.go-version }} check-latest: true - name: Initialize CodeQL - uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 with: languages: go - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 83ba9ceb2..12a0edd58 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -61,6 +61,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 + uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 with: sarif_file: results.sarif From ed930444e27e82d21cd9a70566769296593e839c Mon Sep 17 00:00:00 2001 From: Cameron Rozean Date: Tue, 21 Nov 2023 19:21:15 -0800 Subject: [PATCH 56/83] Correct broken link to quick start guide (#831) Similarly to https://github.com/notaryproject/notaryproject.dev/issues/360, I attempted to follow the link in the README.md for the quick start guide and discovered the broken link. Linking to the official page. URL: https://notaryproject.dev/docs/quickstart/ Expected page: https://notaryproject.dev/docs/quickstart-guides/quickstart-sign-image-artifact/ Signed-off-by: Cameron Rozean --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0316c149c..c50e9f564 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ You can find the Notary Project [README](https://github.com/notaryproject/.githu ## Quick Start -- [Quick start: Sign and validate a container image](https://notaryproject.dev/docs/quickstart-guides/quickstart/) +- [Quick start: Sign and validate a container image](https://notaryproject.dev/docs/quickstart-guides/quickstart-sign-image-artifact/) - [Try out Notation in this Killercoda interactive sandbox environment](https://killercoda.com/notaryproject/scenario/notation) - Build, sign, and verify container images using Notation with [Azure Key Vault](https://docs.microsoft.com/azure/container-registry/container-registry-tutorial-sign-build-push?wt.mc_id=azurelearn_inproduct_oss_notaryproject) or [AWS Signer](https://docs.aws.amazon.com/signer/latest/developerguide/container-workflow.html) - + ## Community Notary Project is a [CNCF Incubating project](https://www.cncf.io/projects/notary/). We :heart: your contribution. From 0d7170a1e295e7b53946815ba23bfa528a64592c Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 27 Nov 2023 12:22:02 +0800 Subject: [PATCH 57/83] updated err msg Signed-off-by: Patrick Zheng --- cmd/notation/plugin/cmd.go | 4 ++-- cmd/notation/plugin/install.go | 40 ++++++++++++++++---------------- cmd/notation/plugin/list.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- test/e2e/suite/plugin/install.go | 4 ++-- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/cmd/notation/plugin/cmd.go b/cmd/notation/plugin/cmd.go index 17f3d3150..f5b676d6f 100644 --- a/cmd/notation/plugin/cmd.go +++ b/cmd/notation/plugin/cmd.go @@ -22,8 +22,8 @@ func Cmd() *cobra.Command { } command.AddCommand( - pluginListCommand(), - pluginInstallCommand(nil), + listCommand(), + installCommand(nil), ) return command diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 5672e0c0d..9452e9352 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -53,7 +53,7 @@ type pluginInstallOpts struct { var ErrNoPluginExecutableFileWasFound = errors.New("no plugin executable file was found") -func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command { +func installCommand(opts *pluginInstallOpts) *cobra.Command { if opts == nil { opts = &pluginInstallOpts{} } @@ -97,7 +97,7 @@ Example - Install plugin from URL, SHA256 checksum is required: } else if opts.isUrl { opts.pluginSourceType = notationplugin.PluginSourceTypeURL } - return installPlugin(cmd, opts) + return install(cmd, opts) }, } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) @@ -110,27 +110,27 @@ Example - Install plugin from URL, SHA256 checksum is required: return command } -func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { +func install(command *cobra.Command, opts *pluginInstallOpts) error { // set log level ctx := opts.LoggingFlagOpts.InitializeLogger(command.Context()) // core process switch opts.pluginSourceType { case notationplugin.PluginSourceTypeFile: - return installFromFileSystem(ctx, opts.pluginSource, opts.inputChecksum, opts.force) + return installPlugin(ctx, opts.pluginSource, opts.inputChecksum, opts.force) case notationplugin.PluginSourceTypeURL: if opts.inputChecksum == "" { - return errors.New("install from URL requires non-empty SHA256 checksum of the plugin source") + return errors.New("installing from URL requires non-empty SHA256 checksum of the plugin source") } pluginURL, err := url.Parse(opts.pluginSource) if err != nil { - return fmt.Errorf("the plugin download failed: %v", err) + return fmt.Errorf("failed to parse plugin download URL %s with error: %w", pluginURL, err) } if pluginURL.Scheme != "https" { - return fmt.Errorf("the plugin download failed: only the HTTPS scheme is supported, but got %s", pluginURL.Scheme) + return fmt.Errorf("failed to download plugin from URL: only the HTTPS scheme is supported, but got %s", pluginURL.Scheme) } tmpFile, err := os.CreateTemp("", notationPluginDownloadTmpFile) if err != nil { - return err + return fmt.Errorf("failed to create notationPluginDownloadTmpFile: %w", err) } defer os.Remove(tmpFile.Name()) defer tmpFile.Close() @@ -138,18 +138,14 @@ func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error { if err != nil { return fmt.Errorf("failed to download plugin from URL %s with error: %w", opts.pluginSource, err) } - downloadPath, err := filepath.Abs(tmpFile.Name()) - if err != nil { - return err - } - return installFromFileSystem(ctx, downloadPath, opts.inputChecksum, opts.force) + return installPlugin(ctx, tmpFile.Name(), opts.inputChecksum, opts.force) default: return errors.New("plugin installation failed: unknown plugin source type") } } -// installFromFileSystem install the plugin from file system -func installFromFileSystem(ctx context.Context, inputPath string, inputChecksum string, force bool) error { +// installPlugin installs the plugin given plugin source path +func installPlugin(ctx context.Context, inputPath string, inputChecksum string, force bool) error { // sanity check inputFileStat, err := os.Stat(inputPath) if err != nil { @@ -175,6 +171,7 @@ func installFromFileSystem(ctx context.Context, inputPath string, inputChecksum if err != nil { return fmt.Errorf("plugin installation failed: %w", err) } + defer rc.Close() if err := installPluginFromFS(ctx, rc, force); err != nil { if errors.Is(err, ErrNoPluginExecutableFileWasFound) { return fmt.Errorf("plugin installation failed: no valid plugin file was found in %s. Plugin file name must in format notation-{plugin-name}", inputPath) @@ -183,7 +180,7 @@ func installFromFileSystem(ctx context.Context, inputPath string, inputChecksum } return nil case notationplugin.MediaTypeGzip: - // when file is gzip, require to be tar + // when file is gzip, required to be tar if err := installPluginFromTarGz(ctx, inputPath, force); err != nil { return fmt.Errorf("plugin installation failed: %w", err) } @@ -193,7 +190,9 @@ func installFromFileSystem(ctx context.Context, inputPath string, inputChecksum } } -// installPluginFromFS extracts , validates and installs the plugin from a fs.FS +// installPluginFromFS extracts, validates and installs the plugin from a fs.FS +// +// Note: zip.ReadCloser implments fs.FS func installPluginFromFS(ctx context.Context, pluginFs fs.FS, force bool) error { // set up logger logger := log.GetLogger(ctx) @@ -204,7 +203,7 @@ func installPluginFromFS(ctx context.Context, pluginFs fs.FS, force bool) error return err } fName := d.Name() - if d.IsDir() && fName != root { // skip any dir in the fs + if d.IsDir() && fName != root { // skip any dir in the fs except root return fs.SkipDir } info, err := d.Info() @@ -214,7 +213,8 @@ func installPluginFromFS(ctx context.Context, pluginFs fs.FS, force bool) error if !info.Mode().IsRegular() || strings.Contains(fName, "..") { return nil } - // validate the file name against the notation-{plugin-name} format + // require one and only one file with name in the format + // `notation-{plugin-name}` logger.Debugf("Processing file %s...", fName) _, err = plugin.ExtractPluginNameFromFileName(fName) if err != nil { @@ -229,6 +229,7 @@ func installPluginFromFS(ctx context.Context, pluginFs fs.FS, force bool) error if err := installPluginExecutable(ctx, fName, rc, force); err != nil { return err } + // on success, skip all remaining files success = true return fs.SkipAll }); err != nil { @@ -268,7 +269,6 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e if !header.FileInfo().Mode().IsRegular() || strings.Contains(header.Name, "..") { continue } - // validate the file name against the notation-{plugin-name} format fName := filepath.Base(header.Name) logger.Debugf("Processing file %s...", fName) _, err = plugin.ExtractPluginNameFromFileName(fName) diff --git a/cmd/notation/plugin/list.go b/cmd/notation/plugin/list.go index a6ae1a504..e715ac233 100644 --- a/cmd/notation/plugin/list.go +++ b/cmd/notation/plugin/list.go @@ -24,7 +24,7 @@ import ( "github.com/spf13/cobra" ) -func pluginListCommand() *cobra.Command { +func listCommand() *cobra.Command { return &cobra.Command{ Use: "list [flags]", Aliases: []string{"ls"}, diff --git a/go.mod b/go.mod index f51f4ed62..acddb46a9 100644 --- a/go.mod +++ b/go.mod @@ -31,4 +31,4 @@ require ( golang.org/x/sys v0.14.0 // indirect ) -replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231124094040-24aef8a46c1e +replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231127041028-b1e008452e56 diff --git a/go.sum b/go.sum index b1ef0441c..09b4352e9 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/Two-Hearts/notation-go v0.0.0-20231124094040-24aef8a46c1e h1:PTr+6mKkHYwLajLtlD4YhsGm7WONPQ7AXshqHa03XR8= -github.com/Two-Hearts/notation-go v0.0.0-20231124094040-24aef8a46c1e/go.mod h1:tSCFsAdKAtB7AfKS/BaUf8AXzASA+9TEokMDEDutqPM= +github.com/Two-Hearts/notation-go v0.0.0-20231127041028-b1e008452e56 h1:N9rJ9ik7rvk7xpldwjHQZs3AmRgrMIdo002mx12eDLg= +github.com/Two-Hearts/notation-go v0.0.0-20231127041028-b1e008452e56/go.mod h1:tSCFsAdKAtB7AfKS/BaUf8AXzASA+9TEokMDEDutqPM= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 9e9bc011c..4efc32925 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -104,14 +104,14 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin URL but missing checksum", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", "--url", PluginURL). - MatchErrContent("Error: install from URL requires non-empty SHA256 checksum of the plugin source\n") + MatchErrContent("Error: installing from URL requires non-empty SHA256 checksum of the plugin source\n") }) }) It("with invalid plugin URL scheme", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", "--url", "http://invalid", "--sha256sum", "abcd"). - MatchErrContent("Error: the plugin download failed: only the HTTPS scheme is supported, but got http\n") + MatchErrContent("Error: failed to download plugin from URL: only the HTTPS scheme is supported, but got http\n") }) }) From 303dac5a20433ab16e9cf851bc07b336698cddaf Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 28 Nov 2023 15:26:31 +0800 Subject: [PATCH 58/83] updated dependency Signed-off-by: Patrick Zheng --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index acddb46a9..659ab9631 100644 --- a/go.mod +++ b/go.mod @@ -31,4 +31,4 @@ require ( golang.org/x/sys v0.14.0 // indirect ) -replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231127041028-b1e008452e56 +replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231128065613-75a090e11a34 diff --git a/go.sum b/go.sum index 09b4352e9..08fa3dcf4 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/Two-Hearts/notation-go v0.0.0-20231127041028-b1e008452e56 h1:N9rJ9ik7rvk7xpldwjHQZs3AmRgrMIdo002mx12eDLg= -github.com/Two-Hearts/notation-go v0.0.0-20231127041028-b1e008452e56/go.mod h1:tSCFsAdKAtB7AfKS/BaUf8AXzASA+9TEokMDEDutqPM= +github.com/Two-Hearts/notation-go v0.0.0-20231128065613-75a090e11a34 h1:iiJ2n15JI32RnMpcMXINz4QlAtamstCfK8rXQKffWfM= +github.com/Two-Hearts/notation-go v0.0.0-20231128065613-75a090e11a34/go.mod h1:tSCFsAdKAtB7AfKS/BaUf8AXzASA+9TEokMDEDutqPM= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= From d27414ba9688dd0716f086bbf40c8f07391ec0b2 Mon Sep 17 00:00:00 2001 From: Yi Zha Date: Tue, 28 Nov 2023 14:37:22 +0800 Subject: [PATCH 59/83] chore: update tag to digest (#837) Signed-off-by: Yi Zha --- specs/commandline/list.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/commandline/list.md b/specs/commandline/list.md index 49726724c..075a80250 100644 --- a/specs/commandline/list.md +++ b/specs/commandline/list.md @@ -49,7 +49,7 @@ notation list /: An example output: ```shell -localhost:5000/net-monitor:v1 +localhost:5000/net-monitor@sha256:8456f085dd609fd12cdebc5f80b6f33f25f670a7a9a03c8fa750b8aee0c4d657 └── application/vnd.cncf.notary.signature ├── sha256:647039638efb22a021f59675c9449dd09956c981a44b82c1ff074513c2c9f273 └── sha256:6bfb3c4fd485d6810f9656ddd4fb603f0c414c5f0b175ef90eeb4090ebd9bfa1 From 3e07ac8f1d2cf36f7a2169500702fdac3d2e6e7a Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Tue, 28 Nov 2023 17:16:39 +0800 Subject: [PATCH 60/83] updated comments Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index ae94352c4..498ded339 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -24,8 +24,11 @@ import ( // MaxPluginSourceBytes specifies the limit on how many bytes are allowed in the // server's response to the download from URL request. +// // It also specifies the limit of a potentail plugin executable file in a // .tar.gz or .zip file. +// +// The plugin source size must be strictly less than this value. var MaxPluginSourceBytes int64 = 256 * 1024 * 1024 // 256 MiB // PluginSourceType is an enum for plugin source From bcb2f5f6e9655081a8677a1f14c30fd0c91e5a65 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 14 Dec 2023 13:43:43 +0800 Subject: [PATCH 61/83] resolved conflicts Signed-off-by: Patrick Zheng --- cmd/notation/plugin/cmd.go | 1 + cmd/notation/plugin/uninstall.go | 107 +++++++++++++++++++++++++++++ test/e2e/suite/plugin/uninstall.go | 38 ++++++++++ 3 files changed, 146 insertions(+) create mode 100644 cmd/notation/plugin/uninstall.go create mode 100644 test/e2e/suite/plugin/uninstall.go diff --git a/cmd/notation/plugin/cmd.go b/cmd/notation/plugin/cmd.go index f5b676d6f..98c494b8f 100644 --- a/cmd/notation/plugin/cmd.go +++ b/cmd/notation/plugin/cmd.go @@ -24,6 +24,7 @@ func Cmd() *cobra.Command { command.AddCommand( listCommand(), installCommand(nil), + uninstallCommand(nil), ) return command 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/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") + }) + }) + +}) From 80f18a7f10de604d48849902234dc39715022a6c Mon Sep 17 00:00:00 2001 From: Yi Zha Date: Fri, 8 Dec 2023 07:52:52 +0800 Subject: [PATCH 62/83] chore: update references with the tag version (#836) Resolve #456 Signed-off-by: Yi Zha Signed-off-by: Yi Zha --- specs/commandline/plugin.md | 2 +- specs/commandline/sign.md | 2 +- specs/commandline/verify.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/commandline/plugin.md b/specs/commandline/plugin.md index 5de10f67b..7e90fe453 100644 --- a/specs/commandline/plugin.md +++ b/specs/commandline/plugin.md @@ -2,7 +2,7 @@ ## Description -Use `notation plugin` to manage plugins. See notation [plugin documentation](https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md) for more details. The `notation plugin` command by itself performs no action. In order to manage notation plugins, one of the subcommands must be used. +Use `notation plugin` to manage plugins. See notation [plugin documentation](https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/plugin-extensibility.md) for more details. The `notation plugin` command by itself performs no action. In order to manage notation plugins, one of the subcommands must be used. ## Outline diff --git a/specs/commandline/sign.md b/specs/commandline/sign.md index 5c3569d5d..ce0de903a 100644 --- a/specs/commandline/sign.md +++ b/specs/commandline/sign.md @@ -63,7 +63,7 @@ Notation uses [OCI image manifest][oci-image-spec] to store signatures in regist ```shell # Prerequisites: -# - A signing plugin is installed. See plugin documentation (https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md) for more details. +# - A signing plugin is installed. See plugin documentation (https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/plugin-extensibility.md) for more details. # - Configure the signing plugin as instructed by plugin vendor. # Add a default signing key referencing the remote key identifier, and the plugin associated with it. diff --git a/specs/commandline/verify.md b/specs/commandline/verify.md index 3fe40a3a4..e529bac37 100644 --- a/specs/commandline/verify.md +++ b/specs/commandline/verify.md @@ -106,7 +106,7 @@ Example values on trust policy properties: | trustedIdentities | "x509.subject: C=US, ST=WA, L=Seattle, O=wabbit-networks.io, OU=Finance, CN=SecureBuilder" | User only trusts the identity with specific subject. User can use `notation certificate show` command to get the `subject` info. | | trustedIdentities | "*" | User trusts any identity (signing certificate) issued by the CA(s) in trust stores. | -User can configure multiple trust policies for different scenarios. See [Trust Policy Schema and properties](https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#trust-policy) for details. +User can configure multiple trust policies for different scenarios. See [Trust Policy Schema and properties](https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy) for details. ### Verify signatures on an OCI artifact stored in a registry From a5e4398c4c7ee9f5b5f9ebdf3421b5d3b7b6d069 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 14 Dec 2023 13:46:21 +0800 Subject: [PATCH 63/83] resolved conflicts Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 3 - cmd/notation/plugin/install.go | 98 +++++++++++--------------- go.mod | 8 +-- go.sum | 16 ++--- internal/osutil/file.go | 29 ++++++++ 5 files changed, 81 insertions(+), 73 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 498ded339..94cf5d13a 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -25,9 +25,6 @@ import ( // MaxPluginSourceBytes specifies the limit on how many bytes are allowed in the // server's response to the download from URL request. // -// It also specifies the limit of a potentail plugin executable file in a -// .tar.gz or .zip file. -// // The plugin source size must be strictly less than this value. var MaxPluginSourceBytes int64 = 256 * 1024 * 1024 // 256 MiB diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 9452e9352..938ec6524 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -190,14 +190,20 @@ func installPlugin(ctx context.Context, inputPath string, inputChecksum string, } } -// installPluginFromFS extracts, validates and installs the plugin from a fs.FS +// installPluginFromFS extracts, validates and installs the plugin files +// from a fs.FS // // Note: zip.ReadCloser implments fs.FS func installPluginFromFS(ctx context.Context, pluginFs fs.FS, force bool) error { // set up logger logger := log.GetLogger(ctx) root := "." - var success bool + // extracting all regular files from root into tmpDir + tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) + if err != nil { + return fmt.Errorf("failed to create notationPluginTmpDir: %w", err) + } + defer os.RemoveAll(tmpDir) if err := fs.WalkDir(pluginFs, root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err @@ -210,35 +216,28 @@ func installPluginFromFS(ctx context.Context, pluginFs fs.FS, force bool) error if err != nil { return err } + // only accept regular files + // it is required by github-advanced-security to check for `..` in fName if !info.Mode().IsRegular() || strings.Contains(fName, "..") { return nil } - // require one and only one file with name in the format - // `notation-{plugin-name}` - logger.Debugf("Processing file %s...", fName) - _, err = plugin.ExtractPluginNameFromFileName(fName) - if err != nil { - logger.Debugf("File name %s is not in format notation-{plugin-name}, skipped", fName) - return nil - } + logger.Debugf("Extracting file %s...", fName) rc, err := pluginFs.Open(path) if err != nil { return err } defer rc.Close() - if err := installPluginExecutable(ctx, fName, rc, force); err != nil { - return err - } - // on success, skip all remaining files - success = true - return fs.SkipAll + tmpFilePath := filepath.Join(tmpDir, fName) + return osutil.CopyFromReaderToDir(rc, tmpFilePath, info.Mode()) }); err != nil { return err } - if !success { - return ErrNoPluginExecutableFileWasFound + // install core process + installOpts := plugin.CLIInstallOptions{ + PluginPath: tmpDir, + Overwrite: force, } - return nil + return installPluginWithOptions(ctx, installOpts) } // installPluginFromTarGz extracts and untar a plugin tar.gz file, validates and @@ -256,8 +255,12 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e } defer decompressedStream.Close() tarReader := tar.NewReader(decompressedStream) - // require one and only one file with name in the format - // `notation-{plugin-name}` + // extracting all regular files into tmpDir + tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) + if err != nil { + return fmt.Errorf("failed to create notationPluginTmpDir: %w", err) + } + defer os.RemoveAll(tmpDir) for { header, err := tarReader.Next() if err != nil { @@ -266,53 +269,32 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e } return err } + // only accept regular files + // it is required by github-advanced-security to check for `..` in fName if !header.FileInfo().Mode().IsRegular() || strings.Contains(header.Name, "..") { continue } fName := filepath.Base(header.Name) - logger.Debugf("Processing file %s...", fName) - _, err = plugin.ExtractPluginNameFromFileName(fName) - if err != nil { - logger.Infof("File name %s is not in format notation-{plugin-name}, skipped", fName) - continue - } - return installPluginExecutable(ctx, fName, tarReader, force) - } - return fmt.Errorf("no valid plugin file was found in %s. Plugin file name must in format notation-{plugin-name}", tarGzPath) -} - -// installPluginExecutable extracts, validates, and installs a plugin executable -// file -func installPluginExecutable(ctx context.Context, fileName string, fileReader io.Reader, force bool) error { - tmpDir, err := os.MkdirTemp("", notationPluginTmpDir) - if err != nil { - return fmt.Errorf("failed to create notationPluginTmpDir: %w", err) - } - defer os.RemoveAll(tmpDir) - tmpFilePath := filepath.Join(tmpDir, fileName) - pluginFile, err := os.OpenFile(tmpFilePath, os.O_WRONLY|os.O_CREATE, 0700) - if err != nil { - return err - } - lr := &io.LimitedReader{ - R: fileReader, - N: notationplugin.MaxPluginSourceBytes, - } - if _, err := io.Copy(pluginFile, lr); err != nil || lr.N == 0 { - _ = pluginFile.Close() - if err != nil { + logger.Debugf("Extracting file %s...", fName) + tmpFilePath := filepath.Join(tmpDir, fName) + if err := osutil.CopyFromReaderToDir(tarReader, tmpFilePath, header.FileInfo().Mode()); err != nil { return err } - return fmt.Errorf("plugin executable file reaches the %d MiB size limit", notationplugin.MaxPluginSourceBytes) } - if err := pluginFile.Close(); err != nil { - return err + // install core process + installOpts := plugin.CLIInstallOptions{ + PluginPath: tmpDir, + Overwrite: force, } - // core process + return installPluginWithOptions(ctx, installOpts) +} + +// installPluginWithOptions installs plugin with CLIInstallOptions +func installPluginWithOptions(ctx context.Context, opts plugin.CLIInstallOptions) error { mgr := plugin.NewCLIManager(dir.PluginFS()) - existingPluginMetadata, newPluginMetadata, err := mgr.Install(ctx, tmpFilePath, force) + existingPluginMetadata, newPluginMetadata, err := mgr.Install(ctx, opts) if err != nil { - if errors.Is(err, &plugin.ErrInstallLowerVersion{}) { + if errors.Is(err, &plugin.PluginDowngradeError{}) { return fmt.Errorf("%s. %w.\nIt is not recommended to install an older version. To force the installation, use the \"--force\" option", newPluginMetadata.Name, err) } return err diff --git a/go.mod b/go.mod index 659ab9631..f95800a1e 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( 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.14.0 + golang.org/x/term v0.15.0 oras.land/oras-go/v2 v2.3.1 ) @@ -25,10 +25,10 @@ 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.15.0 // indirect + golang.org/x/crypto v0.16.0 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect ) -replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231128065613-75a090e11a34 +replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231212122325-c8b1d6e74fbe diff --git a/go.sum b/go.sum index 08fa3dcf4..7919df489 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/Two-Hearts/notation-go v0.0.0-20231128065613-75a090e11a34 h1:iiJ2n15JI32RnMpcMXINz4QlAtamstCfK8rXQKffWfM= -github.com/Two-Hearts/notation-go v0.0.0-20231128065613-75a090e11a34/go.mod h1:tSCFsAdKAtB7AfKS/BaUf8AXzASA+9TEokMDEDutqPM= +github.com/Two-Hearts/notation-go v0.0.0-20231212122325-c8b1d6e74fbe h1:2TKhvhXtXk+I58YgSKwa5EG5vMtaVmmzO47W6x/wcwE= +github.com/Two-Hearts/notation-go v0.0.0-20231212122325-c8b1d6e74fbe/go.mod h1:nqDueF9YCCX0u41Eec7aGJEXgGdM0a3KD79wqhCnxq0= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -51,8 +51,8 @@ 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.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= @@ -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.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.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.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 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 4c7593454..9e04e84cb 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -25,6 +25,10 @@ import ( "strings" ) +// MaxFileBytes is the maximum file bytes. +// When used, the value should strictly less than this number. +var MaxFileBytes int64 = 256 * 1024 * 1024 // 256 MiB + // WriteFile writes to a path with all parent directories created. func WriteFile(path string, data []byte) error { if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { @@ -99,6 +103,31 @@ func IsRegularFile(path string) (bool, error) { return fileStat.Mode().IsRegular(), nil } +// CopyFromReaderToDir copies file from src to dst. The file +// size must be less than 256 MiB. +func CopyFromReaderToDir(src io.Reader, dst string, perm fs.FileMode) error { + dstFile, err := os.Create(dst) + if err != nil { + return err + } + lr := &io.LimitedReader{ + R: src, + N: MaxFileBytes, + } + if _, err := io.Copy(dstFile, lr); err != nil || lr.N == 0 { + _ = dstFile.Close() + if err != nil { + return err + } + return fmt.Errorf("file reaches the %d MiB size limit", MaxFileBytes) + } + if err := dstFile.Chmod(perm); err != nil { + _ = dstFile.Close() + return err + } + return dstFile.Close() +} + // DetectFileType returns a file's content type given path func DetectFileType(path string) (string, error) { rc, err := os.Open(path) From 30651d519fabfb65fb221a866699bd69020e7647 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 03:32:56 +0000 Subject: [PATCH 64/83] build(deps): Bump actions/setup-go from 4.1.0 to 5.0.0 (#845) --- .github/workflows/build.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/release-github.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a42470d77..0bbbdc15b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: fail-fast: true steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ matrix.go-version }} check-latest: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e37b2c66f..d03202f78 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -44,7 +44,7 @@ jobs: - name: Checkout repository uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up Go ${{ matrix.go-version }} environment - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ matrix.go-version }} check-latest: true diff --git a/.github/workflows/release-github.yml b/.github/workflows/release-github.yml index 0b173fd88..6bb5fd097 100644 --- a/.github/workflows/release-github.yml +++ b/.github/workflows/release-github.yml @@ -33,7 +33,7 @@ jobs: fail-fast: true steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ matrix.go-version }} check-latest: true From a5fa33f0c5055501a2c1573524999c3ce22750fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 03:33:18 +0000 Subject: [PATCH 65/83] build(deps): Bump github/codeql-action from 2.22.7 to 2.22.9 (#846) --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d03202f78..bdd5cf5da 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -49,8 +49,8 @@ jobs: go-version: ${{ matrix.go-version }} check-latest: true - name: Initialize CodeQL - uses: github/codeql-action/init@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + uses: github/codeql-action/init@c0d1daa7f7e14667747d73a7dbbe8c074bc8bfe2 # v2.22.9 with: languages: go - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + uses: github/codeql-action/analyze@c0d1daa7f7e14667747d73a7dbbe8c074bc8bfe2 # v2.22.9 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 12a0edd58..add9244f9 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -61,6 +61,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@66b90a5db151a8042fa97405c6cf843bbe433f7b # v2.22.7 + uses: github/codeql-action/upload-sarif@c0d1daa7f7e14667747d73a7dbbe8c074bc8bfe2 # v2.22.9 with: sarif_file: results.sarif From 7550dad64ce6f26d392f93398ebfbe5be7ea20d9 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 14 Dec 2023 13:06:54 +0800 Subject: [PATCH 66/83] added install from executable file directly Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 10 +++++++++- test/e2e/suite/plugin/install.go | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 938ec6524..05b8a3c1a 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -186,7 +186,15 @@ func installPlugin(ctx context.Context, inputPath string, inputChecksum string, } return nil default: - return errors.New("plugin installation failed: invalid file format. Only .tar.gz and .zip formats are supported") + // input file is not in zip or gzip, try install directly + installOpts := plugin.CLIInstallOptions{ + PluginPath: inputPath, + Overwrite: force, + } + if err := installPluginWithOptions(ctx, installOpts); err != nil { + return fmt.Errorf("plugin installation failed: %w", err) + } + return nil } } diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 4efc32925..5e6556581 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -67,10 +67,10 @@ var _ = Describe("notation plugin install", func() { }) }) - It("with invalid plugin file type", func() { + It("with plugin executable file path", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { - notation.ExpectFailure().Exec("plugin", "install", "--file", NotationE2EPluginPath). - MatchErrContent("Error: plugin installation failed: invalid file format. Only .tar.gz and .zip formats are supported\n") + notation.Exec("plugin", "install", "--file", NotationE2EPluginPath). + MatchErrContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") }) }) From 4adfb46a6c7528f2fb3c6f24c3bee28074dd44e8 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 14 Dec 2023 13:23:22 +0800 Subject: [PATCH 67/83] fixing e2e tests Signed-off-by: Patrick Zheng --- test/e2e/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/run.sh b/test/e2e/run.sh index 6365793ea..d82b8addc 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -72,7 +72,7 @@ fi go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.9.5 # build e2e plugin and tar.gz -PLUGIN_NAME=e2e-plugin +PLUGIN_NAME=notation-e2e-plugin ( 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 From 9fdc455b3668dd3f1beeab9001d15e03fa810a69 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 14 Dec 2023 13:35:10 +0800 Subject: [PATCH 68/83] fixing e2e tests Signed-off-by: Patrick Zheng --- test/e2e/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/run.sh b/test/e2e/run.sh index d82b8addc..a3ce61864 100755 --- a/test/e2e/run.sh +++ b/test/e2e/run.sh @@ -73,7 +73,7 @@ go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo@v2.9.5 # build e2e plugin and tar.gz PLUGIN_NAME=notation-e2e-plugin -( 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) +( cd $CWD/plugin && go build -o ./bin/$PLUGIN_NAME . && echo "e2e plugin built." && tar -czvf ./bin/$PLUGIN_NAME.tar.gz -C ./bin/ $PLUGIN_NAME) # setup registry case $REGISTRY_NAME in From f70f73650de63af47782b07212e6d201c32add98 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 14 Dec 2023 13:38:18 +0800 Subject: [PATCH 69/83] fixing e2e tests Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 5e6556581..42692bca0 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -70,7 +70,7 @@ var _ = Describe("notation plugin install", func() { It("with plugin executable file path", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", "--file", NotationE2EPluginPath). - MatchErrContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") + MatchContent("Succussefully installed plugin e2e-plugin, version 1.0.0\n") }) }) From 4257e064cada084bdd1983f36602cbedafa39383 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 14 Dec 2023 16:36:39 +0800 Subject: [PATCH 70/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 6 +++--- cmd/notation/plugin/install.go | 18 ++++++++---------- cmd/notation/registry.go | 3 ++- internal/{auth => httputil}/client.go | 4 ++-- internal/osutil/file.go | 10 +++++----- internal/osutil/file_test.go | 6 +++--- internal/trace/transport.go | 4 ++-- 7 files changed, 25 insertions(+), 26 deletions(-) rename internal/{auth => httputil}/client.go (95%) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 94cf5d13a..d409fb21d 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -19,7 +19,7 @@ import ( "io" "net/http" - notationauth "github.com/notaryproject/notation/internal/auth" + "github.com/notaryproject/notation/internal/httputil" ) // MaxPluginSourceBytes specifies the limit on how many bytes are allowed in the @@ -50,8 +50,8 @@ const ( // DownloadPluginFromURL downloads plugin file from url to a tmp directory func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Writer) error { // Get the data - client := notationauth.NewAuthClient(ctx) - req, err := http.NewRequest("GET", pluginURL, nil) + client := httputil.NewAuthClient(ctx) + req, err := http.NewRequest(http.MethodGet, pluginURL, nil) if err != nil { return err } diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 05b8a3c1a..9b70c2e15 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -47,12 +47,10 @@ type pluginInstallOpts struct { pluginSource string inputChecksum string isFile bool - isUrl bool + isURL bool force bool } -var ErrNoPluginExecutableFileWasFound = errors.New("no plugin executable file was found") - func installCommand(opts *pluginInstallOpts) *cobra.Command { if opts == nil { opts = &pluginInstallOpts{} @@ -83,18 +81,21 @@ Example - Install plugin from URL, SHA256 checksum is required: if opts.isFile { return errors.New("missing plugin file path") } - if opts.isUrl { + if opts.isURL { return errors.New("missing plugin URL") } return errors.New("missing plugin source") } + if len(args) > 1 { + return fmt.Errorf("can only insall one plugin at a time, but got %v", args) + } opts.pluginSource = args[0] return nil }, RunE: func(cmd *cobra.Command, args []string) error { if opts.isFile { opts.pluginSourceType = notationplugin.PluginSourceTypeFile - } else if opts.isUrl { + } else if opts.isURL { opts.pluginSourceType = notationplugin.PluginSourceTypeURL } return install(cmd, opts) @@ -102,7 +103,7 @@ Example - Install plugin from URL, SHA256 checksum is required: } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) command.Flags().BoolVar(&opts.isFile, "file", false, "install plugin from a file in file system") - command.Flags().BoolVar(&opts.isUrl, "url", false, "install plugin from an HTTPS URL") + command.Flags().BoolVar(&opts.isURL, "url", false, "install plugin from an HTTPS URL") command.Flags().StringVar(&opts.inputChecksum, "sha256sum", "", "must match SHA256 of the plugin source, required when \"--url\" flag is set") command.Flags().BoolVar(&opts.force, "force", false, "force the installation of the plugin") command.MarkFlagsMutuallyExclusive("file", "url") @@ -156,7 +157,7 @@ func installPlugin(ctx context.Context, inputPath string, inputChecksum string, } // checksum check if inputChecksum != "" { - if err := osutil.ValidateChecksum(inputPath, inputChecksum); err != nil { + if err := osutil.ValidateSHA256Sum(inputPath, inputChecksum); err != nil { return fmt.Errorf("plugin installation failed: %w", err) } } @@ -173,9 +174,6 @@ func installPlugin(ctx context.Context, inputPath string, inputChecksum string, } defer rc.Close() if err := installPluginFromFS(ctx, rc, force); err != nil { - if errors.Is(err, ErrNoPluginExecutableFileWasFound) { - return fmt.Errorf("plugin installation failed: no valid plugin file was found in %s. Plugin file name must in format notation-{plugin-name}", inputPath) - } return fmt.Errorf("plugin installation failed: %w", err) } return nil diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 9cb06943e..14cfdf3ec 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -23,6 +23,7 @@ import ( notationregistry "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation/cmd/notation/internal/experimental" notationauth "github.com/notaryproject/notation/internal/auth" + "github.com/notaryproject/notation/internal/httputil" "github.com/notaryproject/notation/pkg/configutil" credentials "github.com/oras-project/oras-credentials-go" "oras.land/oras-go/v2/registry" @@ -140,7 +141,7 @@ func getAuthClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Refer } // build authClient - authClient := notationauth.NewAuthClient(ctx) + authClient := httputil.NewAuthClient(ctx) if !withCredential { return authClient, insecureRegistry, nil } diff --git a/internal/auth/client.go b/internal/httputil/client.go similarity index 95% rename from internal/auth/client.go rename to internal/httputil/client.go index 410bc8e93..aeb1b43e0 100644 --- a/internal/auth/client.go +++ b/internal/httputil/client.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package auth +package httputil import ( "context" @@ -28,6 +28,6 @@ func NewAuthClient(ctx context.Context) *auth.Client { ClientID: "notation", } client.SetUserAgent("notation/" + version.GetVersion()) - trace.SetHttpDebugLog(ctx, client) + trace.SetHTTPDebugLog(ctx, client) return client } diff --git a/internal/osutil/file.go b/internal/osutil/file.go index 9e04e84cb..d8fcdbebb 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -136,15 +136,15 @@ func DetectFileType(path string) (string, error) { } defer rc.Close() lr := io.LimitReader(rc, 512) - header := make([]byte, 512) - if _, err := lr.Read(header); err != nil { + header, err := io.ReadAll(lr) + if err != nil { return "", err } return http.DetectContentType(header), nil } -// ValidateChecksum returns nil if SHA256 of file at path equals to checksum. -func ValidateChecksum(path string, checksum string) error { +// ValidateSHA256Sum returns nil if SHA256 of file at path equals to checksum. +func ValidateSHA256Sum(path string, checksum string) error { rc, err := os.Open(path) if err != nil { return err @@ -156,7 +156,7 @@ func ValidateChecksum(path string, checksum string) error { } sha256sum := sha256Hash.Sum(nil) enc := strings.ToLower(hex.EncodeToString(sha256sum[:])) - if enc != strings.ToLower(checksum) { + if !strings.EqualFold(enc, checksum) { return fmt.Errorf("plugin checksum does not match user input. Expecting %s", checksum) } return nil diff --git a/internal/osutil/file_test.go b/internal/osutil/file_test.go index cc8218864..fdf9dd331 100644 --- a/internal/osutil/file_test.go +++ b/internal/osutil/file_test.go @@ -26,7 +26,7 @@ func validFileContent(t *testing.T, filename string, content []byte) { if err != nil { t.Fatal(err) } - if bytes.Compare(content, b) != 0 { + if !bytes.Equal(content, b) { t.Fatal("file content is not correct") } } @@ -262,10 +262,10 @@ func TestCopyToDir(t *testing.T) { func TestValidateChecksum(t *testing.T) { expectedErrorMsg := "plugin checksum does not match user input. Expecting abcd123" - if err := ValidateChecksum("./testdata/test", "abcd123"); err == nil || err.Error() != expectedErrorMsg { + if err := ValidateSHA256Sum("./testdata/test", "abcd123"); err == nil || err.Error() != expectedErrorMsg { t.Fatalf("expected err %s, got %v", expectedErrorMsg, err) } - if err := ValidateChecksum("./testdata/test", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); err != nil { + if err := ValidateSHA256Sum("./testdata/test", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); err != nil { t.Fatalf("expected nil err, got %v", err) } } diff --git a/internal/trace/transport.go b/internal/trace/transport.go index 728d678a0..86700c692 100644 --- a/internal/trace/transport.go +++ b/internal/trace/transport.go @@ -86,8 +86,8 @@ func logHeader(header http.Header, e log.Logger) { } } -// SetHttpDebugLog sets up http debug log with logrus.Logger -func SetHttpDebugLog(ctx context.Context, authClient *auth.Client) { +// SetHTTPDebugLog sets up http debug log with logrus.Logger +func SetHTTPDebugLog(ctx context.Context, authClient *auth.Client) { if logrusLog, ok := log.GetLogger(ctx).(*logrus.Logger); ok && logrusLog.Level != logrus.DebugLevel { return } From 1357ed37cf044081a0dcf86c4b79d5a36de148f8 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Fri, 15 Dec 2023 18:47:11 +0800 Subject: [PATCH 71/83] updated dependency Signed-off-by: Patrick Zheng --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f95800a1e..2ecd55189 100644 --- a/go.mod +++ b/go.mod @@ -31,4 +31,4 @@ require ( golang.org/x/sys v0.15.0 // indirect ) -replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231212122325-c8b1d6e74fbe +replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231215104220-cb3218292bdf diff --git a/go.sum b/go.sum index 7919df489..809758a8b 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/Two-Hearts/notation-go v0.0.0-20231212122325-c8b1d6e74fbe h1:2TKhvhXtXk+I58YgSKwa5EG5vMtaVmmzO47W6x/wcwE= -github.com/Two-Hearts/notation-go v0.0.0-20231212122325-c8b1d6e74fbe/go.mod h1:nqDueF9YCCX0u41Eec7aGJEXgGdM0a3KD79wqhCnxq0= +github.com/Two-Hearts/notation-go v0.0.0-20231215104220-cb3218292bdf h1:xLqJSkI+5+5j7vvEV7pWbH/gmb9y0ibJzfi3Ts3MIlM= +github.com/Two-Hearts/notation-go v0.0.0-20231215104220-cb3218292bdf/go.mod h1:nqDueF9YCCX0u41Eec7aGJEXgGdM0a3KD79wqhCnxq0= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= From 34be1e13689c57c251499e79e006d05fe788a1ab Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 18 Dec 2023 11:05:20 +0800 Subject: [PATCH 72/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 39 ++++++++++++++++------------------ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 9b70c2e15..6e616bd8d 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -117,7 +117,10 @@ func install(command *cobra.Command, opts *pluginInstallOpts) error { // core process switch opts.pluginSourceType { case notationplugin.PluginSourceTypeFile: - return installPlugin(ctx, opts.pluginSource, opts.inputChecksum, opts.force) + if err := installPlugin(ctx, opts.pluginSource, opts.inputChecksum, opts.force); err != nil { + return fmt.Errorf("plugin installation failed: %w", err) + } + return nil case notationplugin.PluginSourceTypeURL: if opts.inputChecksum == "" { return errors.New("installing from URL requires non-empty SHA256 checksum of the plugin source") @@ -139,7 +142,10 @@ func install(command *cobra.Command, opts *pluginInstallOpts) error { if err != nil { return fmt.Errorf("failed to download plugin from URL %s with error: %w", opts.pluginSource, err) } - return installPlugin(ctx, tmpFile.Name(), opts.inputChecksum, opts.force) + if err := installPlugin(ctx, tmpFile.Name(), opts.inputChecksum, opts.force); err != nil { + return fmt.Errorf("plugin installation failed: %w", err) + } + return nil default: return errors.New("plugin installation failed: unknown plugin source type") } @@ -150,49 +156,40 @@ func installPlugin(ctx context.Context, inputPath string, inputChecksum string, // sanity check inputFileStat, err := os.Stat(inputPath) if err != nil { - return fmt.Errorf("plugin installation failed: %w", err) + return err } if !inputFileStat.Mode().IsRegular() { - return fmt.Errorf("plugin installation failed: %s is not a valid file", inputPath) + return fmt.Errorf("%s is not a valid file", inputPath) } // checksum check if inputChecksum != "" { if err := osutil.ValidateSHA256Sum(inputPath, inputChecksum); err != nil { - return fmt.Errorf("plugin installation failed: %w", err) + return err } } // install the plugin based on file type fileType, err := osutil.DetectFileType(inputPath) if err != nil { - return fmt.Errorf("plugin installation failed: %w", err) + return err } switch fileType { case notationplugin.MediaTypeZip: rc, err := zip.OpenReader(inputPath) if err != nil { - return fmt.Errorf("plugin installation failed: %w", err) + return err } defer rc.Close() - if err := installPluginFromFS(ctx, rc, force); err != nil { - return fmt.Errorf("plugin installation failed: %w", err) - } - return nil + return installPluginFromFS(ctx, rc, force) case notationplugin.MediaTypeGzip: // when file is gzip, required to be tar - if err := installPluginFromTarGz(ctx, inputPath, force); err != nil { - return fmt.Errorf("plugin installation failed: %w", err) - } - return nil + return installPluginFromTarGz(ctx, inputPath, force) default: // input file is not in zip or gzip, try install directly installOpts := plugin.CLIInstallOptions{ PluginPath: inputPath, Overwrite: force, } - if err := installPluginWithOptions(ctx, installOpts); err != nil { - return fmt.Errorf("plugin installation failed: %w", err) - } - return nil + return installPluginWithOptions(ctx, installOpts) } } @@ -222,7 +219,7 @@ func installPluginFromFS(ctx context.Context, pluginFs fs.FS, force bool) error if err != nil { return err } - // only accept regular files + // only accept regular files. // it is required by github-advanced-security to check for `..` in fName if !info.Mode().IsRegular() || strings.Contains(fName, "..") { return nil @@ -275,7 +272,7 @@ func installPluginFromTarGz(ctx context.Context, tarGzPath string, force bool) e } return err } - // only accept regular files + // only accept regular files. // it is required by github-advanced-security to check for `..` in fName if !header.FileInfo().Mode().IsRegular() || strings.Contains(header.Name, "..") { continue diff --git a/go.mod b/go.mod index 2ecd55189..97fc1a258 100644 --- a/go.mod +++ b/go.mod @@ -31,4 +31,4 @@ require ( golang.org/x/sys v0.15.0 // indirect ) -replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231215104220-cb3218292bdf +replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231218020912-67d267e3c892 diff --git a/go.sum b/go.sum index 809758a8b..7219616b6 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/Two-Hearts/notation-go v0.0.0-20231215104220-cb3218292bdf h1:xLqJSkI+5+5j7vvEV7pWbH/gmb9y0ibJzfi3Ts3MIlM= -github.com/Two-Hearts/notation-go v0.0.0-20231215104220-cb3218292bdf/go.mod h1:nqDueF9YCCX0u41Eec7aGJEXgGdM0a3KD79wqhCnxq0= +github.com/Two-Hearts/notation-go v0.0.0-20231218020912-67d267e3c892 h1:zTvkhnjxvTPInRsGYV0fsnAugLY/aAtOFWEJ1arx/6M= +github.com/Two-Hearts/notation-go v0.0.0-20231218020912-67d267e3c892/go.mod h1:nqDueF9YCCX0u41Eec7aGJEXgGdM0a3KD79wqhCnxq0= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= From 69b33e0f5f9d2ff7d2387b2528cefd2c466cb63d Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 18 Dec 2023 12:03:16 +0800 Subject: [PATCH 73/83] updated error message Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 3 ++- cmd/notation/plugin/install.go | 5 +++-- cmd/notation/registry.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- internal/httputil/client.go | 4 +++- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index d409fb21d..d5918c9c6 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -18,6 +18,7 @@ import ( "fmt" "io" "net/http" + "time" "github.com/notaryproject/notation/internal/httputil" ) @@ -50,7 +51,7 @@ const ( // DownloadPluginFromURL downloads plugin file from url to a tmp directory func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Writer) error { // Get the data - client := httputil.NewAuthClient(ctx) + client := httputil.NewAuthClient(ctx, &http.Client{Timeout: 100 * time.Second}) req, err := http.NewRequest(http.MethodGet, pluginURL, nil) if err != nil { return err diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 6e616bd8d..a9644a5f9 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -297,8 +297,9 @@ func installPluginWithOptions(ctx context.Context, opts plugin.CLIInstallOptions mgr := plugin.NewCLIManager(dir.PluginFS()) existingPluginMetadata, newPluginMetadata, err := mgr.Install(ctx, opts) if err != nil { - if errors.Is(err, &plugin.PluginDowngradeError{}) { - return fmt.Errorf("%s. %w.\nIt is not recommended to install an older version. To force the installation, use the \"--force\" option", newPluginMetadata.Name, err) + var errPluginDowngrade plugin.PluginDowngradeError + if errors.As(err, &errPluginDowngrade) { + return fmt.Errorf("%w.\nIt is not recommended to install an older version. To force the installation, use the \"--force\" option", errPluginDowngrade) } return err } diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 14cfdf3ec..fe82b6c6e 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -141,7 +141,7 @@ func getAuthClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Refer } // build authClient - authClient := httputil.NewAuthClient(ctx) + authClient := httputil.NewAuthClient(ctx, nil) if !withCredential { return authClient, insecureRegistry, nil } diff --git a/go.mod b/go.mod index 97fc1a258..a2120b2ec 100644 --- a/go.mod +++ b/go.mod @@ -31,4 +31,4 @@ require ( golang.org/x/sys v0.15.0 // indirect ) -replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231218020912-67d267e3c892 +replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231218035538-d6ab20f72f99 diff --git a/go.sum b/go.sum index 7219616b6..4620de785 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/Two-Hearts/notation-go v0.0.0-20231218020912-67d267e3c892 h1:zTvkhnjxvTPInRsGYV0fsnAugLY/aAtOFWEJ1arx/6M= -github.com/Two-Hearts/notation-go v0.0.0-20231218020912-67d267e3c892/go.mod h1:nqDueF9YCCX0u41Eec7aGJEXgGdM0a3KD79wqhCnxq0= +github.com/Two-Hearts/notation-go v0.0.0-20231218035538-d6ab20f72f99 h1:HXyIHsXTT/GeWRGuRnLlHGEYPcPIKdmhbyS7j8Yw4PA= +github.com/Two-Hearts/notation-go v0.0.0-20231218035538-d6ab20f72f99/go.mod h1:nqDueF9YCCX0u41Eec7aGJEXgGdM0a3KD79wqhCnxq0= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= diff --git a/internal/httputil/client.go b/internal/httputil/client.go index aeb1b43e0..ee0324ed5 100644 --- a/internal/httputil/client.go +++ b/internal/httputil/client.go @@ -15,6 +15,7 @@ package httputil import ( "context" + "net/http" "github.com/notaryproject/notation/internal/trace" "github.com/notaryproject/notation/internal/version" @@ -22,8 +23,9 @@ import ( ) // NewAuthClient returns an *auth.Client -func NewAuthClient(ctx context.Context) *auth.Client { +func NewAuthClient(ctx context.Context, httpClient *http.Client) *auth.Client { client := &auth.Client{ + Client: httpClient, Cache: auth.NewCache(), ClientID: "notation", } From 3266871c5344162c14d2f2c0a201efaf4a5ad1a7 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 18 Dec 2023 21:27:36 +0800 Subject: [PATCH 74/83] updated notation-go dependency Signed-off-by: Patrick Zheng --- go.mod | 4 +--- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index a2120b2ec..ba5c0ce59 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21 require ( github.com/notaryproject/notation-core-go v1.0.1 - github.com/notaryproject/notation-go v1.0.2-0.20231123031546-5de0d58b21c1 + github.com/notaryproject/notation-go v1.0.2-0.20231218132318-85a5bb9826c6 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 @@ -30,5 +30,3 @@ require ( golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.15.0 // indirect ) - -replace github.com/notaryproject/notation-go => github.com/Two-Hearts/notation-go v0.0.0-20231218035538-d6ab20f72f99 diff --git a/go.sum b/go.sum index 4620de785..fe89f76e9 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= -github.com/Two-Hearts/notation-go v0.0.0-20231218035538-d6ab20f72f99 h1:HXyIHsXTT/GeWRGuRnLlHGEYPcPIKdmhbyS7j8Yw4PA= -github.com/Two-Hearts/notation-go v0.0.0-20231218035538-d6ab20f72f99/go.mod h1:nqDueF9YCCX0u41Eec7aGJEXgGdM0a3KD79wqhCnxq0= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -22,6 +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.2-0.20231218132318-85a5bb9826c6 h1:9YgUKLuNU8eNlv2H696aBQzW8CtSjevRgbMGld59wrY= +github.com/notaryproject/notation-go v1.0.2-0.20231218132318-85a5bb9826c6/go.mod h1:nqDueF9YCCX0u41Eec7aGJEXgGdM0a3KD79wqhCnxq0= 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= From 9a6bc30b6b1a1e185469a590b43a669e4653c162 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Mon, 18 Dec 2023 21:38:38 +0800 Subject: [PATCH 75/83] updated func doc Signed-off-by: Patrick Zheng --- internal/osutil/file.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/osutil/file.go b/internal/osutil/file.go index d8fcdbebb..e2c680b33 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -103,8 +103,8 @@ func IsRegularFile(path string) (bool, error) { return fileStat.Mode().IsRegular(), nil } -// CopyFromReaderToDir copies file from src to dst. The file -// size must be less than 256 MiB. +// CopyFromReaderToDir copies file from src to dst where dst is the destination +// file path. The file size must be less than 256 MiB. func CopyFromReaderToDir(src io.Reader, dst string, perm fs.FileMode) error { dstFile, err := os.Create(dst) if err != nil { From 489136aed089a46197d1eff3843f73c0787a3de7 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 20 Dec 2023 10:30:54 +0800 Subject: [PATCH 76/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 4 ++-- cmd/notation/plugin/install.go | 11 ++++++----- internal/osutil/file.go | 2 +- specs/commandline/plugin.md | 14 +------------- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index d5918c9c6..3ac43240d 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -51,7 +51,7 @@ const ( // DownloadPluginFromURL downloads plugin file from url to a tmp directory func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Writer) error { // Get the data - client := httputil.NewAuthClient(ctx, &http.Client{Timeout: 100 * time.Second}) + client := httputil.NewAuthClient(ctx, &http.Client{Timeout: 20 * time.Minute}) req, err := http.NewRequest(http.MethodGet, pluginURL, nil) if err != nil { return err @@ -63,7 +63,7 @@ func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Wri defer resp.Body.Close() // Check server response if resp.StatusCode != http.StatusOK { - return fmt.Errorf("https response bad status: %s", resp.Status) + return fmt.Errorf("%s %q: https response bad status: %s", resp.Request.Method, resp.Request.URL, resp.Status) } // Write the body to file lr := &io.LimitedReader{ diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index a9644a5f9..53956a0a9 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -78,10 +78,10 @@ Example - Install plugin from URL, SHA256 checksum is required: `, Args: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - if opts.isFile { + switch { + case opts.isFile: return errors.New("missing plugin file path") - } - if opts.isURL { + case opts.isURL: return errors.New("missing plugin URL") } return errors.New("missing plugin source") @@ -93,9 +93,10 @@ Example - Install plugin from URL, SHA256 checksum is required: return nil }, RunE: func(cmd *cobra.Command, args []string) error { - if opts.isFile { + switch { + case opts.isFile: opts.pluginSourceType = notationplugin.PluginSourceTypeFile - } else if opts.isURL { + case opts.isURL: opts.pluginSourceType = notationplugin.PluginSourceTypeURL } return install(cmd, opts) diff --git a/internal/osutil/file.go b/internal/osutil/file.go index e2c680b33..d2de17ce5 100644 --- a/internal/osutil/file.go +++ b/internal/osutil/file.go @@ -155,7 +155,7 @@ func ValidateSHA256Sum(path string, checksum string) error { return err } sha256sum := sha256Hash.Sum(nil) - enc := strings.ToLower(hex.EncodeToString(sha256sum[:])) + enc := hex.EncodeToString(sha256sum[:]) if !strings.EqualFold(enc, checksum) { return fmt.Errorf("plugin checksum does not match user input. Expecting %s", checksum) } diff --git a/specs/commandline/plugin.md b/specs/commandline/plugin.md index d8c97442e..8ab20e54f 100644 --- a/specs/commandline/plugin.md +++ b/specs/commandline/plugin.md @@ -95,11 +95,7 @@ Successfully installed plugin , version If the entered plugin checksum digest doesn't match the published checksum, Notation will return an error message and will not start installation. ```console -<<<<<<< HEAD Error: plugin installation failed: plugin checksum does not match user input. Expecting -======= -Error: failed to install the plugin: plugin checksum does not match user input. Expecting ->>>>>>> 324b79ca03e4c51d9071f006e31833823d9f9806 ``` If the plugin version is higher than the existing plugin, Notation will start installation and overwrite the existing plugin. @@ -111,21 +107,13 @@ Successfully installed plugin , updated the version from to If the plugin version is equal to the existing plugin, Notation will not start installation and return the following message. Users can use a flag `--force` to skip plugin version check and force the installation. ```console -<<<<<<< HEAD Error: plugin installation failed: plugin with version already exists. -======= -Error: failed to install the plugin: with version already exists. ->>>>>>> 324b79ca03e4c51d9071f006e31833823d9f9806 ``` If the plugin version is lower than the existing plugin, Notation will return an error message and will not start installation. Users can use a flag `--force` to skip plugin version check and force the installation. ```console -<<<<<<< HEAD -Error: plugin installation failed: . The installing plugin version is lower than the existing plugin version . -======= -Error: failed to install the plugin: . The installing plugin version is lower than the existing plugin version . ->>>>>>> 324b79ca03e4c51d9071f006e31833823d9f9806 +Error: failed to install plugin: . The installing plugin version is lower than the existing plugin version . It is not recommended to install an older version. To force the installation, use the "--force" option. ``` ### Install a plugin from URL From 534199fa69bd74e8de04ce44110775ba8ca94c3f Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 20 Dec 2023 10:36:14 +0800 Subject: [PATCH 77/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 53956a0a9..152b68e4f 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -104,7 +104,7 @@ Example - Install plugin from URL, SHA256 checksum is required: } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) command.Flags().BoolVar(&opts.isFile, "file", false, "install plugin from a file in file system") - command.Flags().BoolVar(&opts.isURL, "url", false, "install plugin from an HTTPS URL") + command.Flags().BoolVar(&opts.isURL, "url", false, "install plugin from an HTTPS URL. The timeout of the download HTTPS request is set to 20 minutes") command.Flags().StringVar(&opts.inputChecksum, "sha256sum", "", "must match SHA256 of the plugin source, required when \"--url\" flag is set") command.Flags().BoolVar(&opts.force, "force", false, "force the installation of the plugin") command.MarkFlagsMutuallyExclusive("file", "url") From 0d510bc6134a439527147872809bdec4b00a8b89 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 20 Dec 2023 10:38:03 +0800 Subject: [PATCH 78/83] updated per code review Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 3ac43240d..82bb70bba 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -75,7 +75,7 @@ func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Wri return err } if lr.N == 0 { - return fmt.Errorf("https response reaches the %d MiB size limit", MaxPluginSourceBytes) + return fmt.Errorf("%s %q: https response reaches the %d MiB size limit", resp.Request.Method, resp.Request.URL, MaxPluginSourceBytes) } return nil } From a68d0987c3bd242e3ad356a091b904fb66ff760b Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Wed, 20 Dec 2023 16:39:37 +0800 Subject: [PATCH 79/83] update Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 2 +- cmd/notation/plugin/install.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 82bb70bba..9de57fb38 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -51,7 +51,7 @@ const ( // DownloadPluginFromURL downloads plugin file from url to a tmp directory func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Writer) error { // Get the data - client := httputil.NewAuthClient(ctx, &http.Client{Timeout: 20 * time.Minute}) + client := httputil.NewAuthClient(ctx, &http.Client{Timeout: 10 * time.Minute}) req, err := http.NewRequest(http.MethodGet, pluginURL, nil) if err != nil { return err diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 152b68e4f..843c4f9e2 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -104,7 +104,7 @@ Example - Install plugin from URL, SHA256 checksum is required: } opts.LoggingFlagOpts.ApplyFlags(command.Flags()) command.Flags().BoolVar(&opts.isFile, "file", false, "install plugin from a file in file system") - command.Flags().BoolVar(&opts.isURL, "url", false, "install plugin from an HTTPS URL. The timeout of the download HTTPS request is set to 20 minutes") + command.Flags().BoolVar(&opts.isURL, "url", false, "install plugin from an HTTPS URL. The timeout of the download HTTPS request is set to 10 minutes") command.Flags().StringVar(&opts.inputChecksum, "sha256sum", "", "must match SHA256 of the plugin source, required when \"--url\" flag is set") command.Flags().BoolVar(&opts.force, "force", false, "force the installation of the plugin") command.MarkFlagsMutuallyExclusive("file", "url") From 49a3bcbc36b8c58d5f8a52559b60e721e370b308 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 21 Dec 2023 08:44:14 +0800 Subject: [PATCH 80/83] added printouts to show dowloading plugin from URL Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 843c4f9e2..3eefe4018 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -139,10 +139,12 @@ func install(command *cobra.Command, opts *pluginInstallOpts) error { } defer os.Remove(tmpFile.Name()) defer tmpFile.Close() + fmt.Printf("Downloading plugin from %s\n", opts.pluginSource) err = notationplugin.DownloadPluginFromURL(ctx, opts.pluginSource, tmpFile) if err != nil { return fmt.Errorf("failed to download plugin from URL %s with error: %w", opts.pluginSource, err) } + fmt.Printf("Download completed. Saved to tmp file %s.\n", tmpFile.Name()) if err := installPlugin(ctx, tmpFile.Name(), opts.inputChecksum, opts.force); err != nil { return fmt.Errorf("plugin installation failed: %w", err) } From af4b648a5dfd83b734922e9bcc89dd47a4f59297 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 21 Dec 2023 08:57:46 +0800 Subject: [PATCH 81/83] fix E2E test Signed-off-by: Patrick Zheng --- cmd/notation/plugin/install.go | 2 +- test/e2e/suite/plugin/install.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index 3eefe4018..feed6b62c 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -144,7 +144,7 @@ func install(command *cobra.Command, opts *pluginInstallOpts) error { if err != nil { return fmt.Errorf("failed to download plugin from URL %s with error: %w", opts.pluginSource, err) } - fmt.Printf("Download completed. Saved to tmp file %s.\n", tmpFile.Name()) + fmt.Printf("Download completed. Saved to tmp file %s\n", tmpFile.Name()) if err := installPlugin(ctx, tmpFile.Name(), opts.inputChecksum, opts.force); err != nil { return fmt.Errorf("plugin installation failed: %w", err) } diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 42692bca0..5dd2d7952 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -104,7 +104,7 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin URL but missing checksum", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", "--url", PluginURL). - MatchErrContent("Error: installing from URL requires non-empty SHA256 checksum of the plugin source\n") + MatchKeyWords("Error: installing from URL requires non-empty SHA256 checksum of the plugin source\n") }) }) From 57c6b35a47f9db4afb270eea9e291a1407a3fa26 Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 21 Dec 2023 09:04:45 +0800 Subject: [PATCH 82/83] fix E2E test Signed-off-by: Patrick Zheng --- test/e2e/suite/plugin/install.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/suite/plugin/install.go b/test/e2e/suite/plugin/install.go index 5dd2d7952..bd85d0389 100644 --- a/test/e2e/suite/plugin/install.go +++ b/test/e2e/suite/plugin/install.go @@ -97,14 +97,14 @@ var _ = Describe("notation plugin install", func() { It("with valid plugin URL", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.Exec("plugin", "install", "--url", PluginURL, "--sha256sum", PluginChecksum). - MatchContent("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") + MatchKeyWords("Succussefully installed plugin e2e-test-plugin, version 0.1.0\n") }) }) It("with valid plugin URL but missing checksum", func() { Host(nil, func(notation *utils.ExecOpts, _ *Artifact, vhost *utils.VirtualHost) { notation.ExpectFailure().Exec("plugin", "install", "--url", PluginURL). - MatchKeyWords("Error: installing from URL requires non-empty SHA256 checksum of the plugin source\n") + MatchErrContent("Error: installing from URL requires non-empty SHA256 checksum of the plugin source\n") }) }) From 45788aeadc295f2d5929556c341c4a2540f91d7c Mon Sep 17 00:00:00 2001 From: Patrick Zheng Date: Thu, 21 Dec 2023 14:41:17 +0800 Subject: [PATCH 83/83] update Signed-off-by: Patrick Zheng --- cmd/notation/internal/plugin/plugin.go | 6 +++++- cmd/notation/plugin/install.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/notation/internal/plugin/plugin.go b/cmd/notation/internal/plugin/plugin.go index 9de57fb38..e06f57c18 100644 --- a/cmd/notation/internal/plugin/plugin.go +++ b/cmd/notation/internal/plugin/plugin.go @@ -48,10 +48,14 @@ const ( MediaTypeGzip = "application/x-gzip" ) +// DownloadPluginFromURLTimeout is the timeout when downloading plugin from a +// URL +const DownloadPluginFromURLTimeout = 10 * time.Minute + // DownloadPluginFromURL downloads plugin file from url to a tmp directory func DownloadPluginFromURL(ctx context.Context, pluginURL string, tmpFile io.Writer) error { // Get the data - client := httputil.NewAuthClient(ctx, &http.Client{Timeout: 10 * time.Minute}) + client := httputil.NewAuthClient(ctx, &http.Client{Timeout: DownloadPluginFromURLTimeout}) req, err := http.NewRequest(http.MethodGet, pluginURL, nil) if err != nil { return err diff --git a/cmd/notation/plugin/install.go b/cmd/notation/plugin/install.go index feed6b62c..2dfed9093 100644 --- a/cmd/notation/plugin/install.go +++ b/cmd/notation/plugin/install.go @@ -144,7 +144,7 @@ func install(command *cobra.Command, opts *pluginInstallOpts) error { if err != nil { return fmt.Errorf("failed to download plugin from URL %s with error: %w", opts.pluginSource, err) } - fmt.Printf("Download completed. Saved to tmp file %s\n", tmpFile.Name()) + fmt.Println("Download completed") if err := installPlugin(ctx, tmpFile.Name(), opts.inputChecksum, opts.force); err != nil { return fmt.Errorf("plugin installation failed: %w", err) }