Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Zheng <[email protected]>
  • Loading branch information
Two-Hearts committed Nov 15, 2023
1 parent ccd83e0 commit c9e0a37
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 99 deletions.
8 changes: 4 additions & 4 deletions cmd/notation/internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
40 changes: 20 additions & 20 deletions cmd/notation/internal/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
}
8 changes: 4 additions & 4 deletions cmd/notation/internal/plugin/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,29 +61,29 @@ 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)
}
if pluginName != "my-plugin" {
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)
}
if pluginName != "my-plugin" {
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)
Expand Down
93 changes: 42 additions & 51 deletions cmd/notation/plugin/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"net/url"
"os"
"path/filepath"
Expand All @@ -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"
Expand All @@ -39,7 +37,6 @@ import (

const (
notationPluginTmpDir = "notationPluginTmpDir"
httpsPrefix = "https://"
)

type pluginInstallOpts struct {
Expand Down Expand Up @@ -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
}
Expand All @@ -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")
}
}

Expand All @@ -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")
}
}

Expand All @@ -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, &notationerrors.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
Expand All @@ -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 {
Expand All @@ -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, &notationerrors.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)
Expand Down Expand Up @@ -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
}
Expand Down
8 changes: 1 addition & 7 deletions internal/osutil/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
package osutil

import (
"errors"
"fmt"
"io"
"io/fs"
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Loading

0 comments on commit c9e0a37

Please sign in to comment.