Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add install method to plugin CLIManager #364

Merged
merged 38 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e561df3
added install to plugin CLIManager
Two-Hearts Nov 24, 2023
6aabbeb
update
Two-Hearts Nov 24, 2023
aa458a8
update
Two-Hearts Nov 24, 2023
59101ed
updated error messages
Two-Hearts Nov 24, 2023
24aef8a
updated error messages
Two-Hearts Nov 24, 2023
b1e0084
updated err msg
Two-Hearts Nov 27, 2023
67bf256
Merge branch 'notaryproject:main' into plugin
Two-Hearts Nov 27, 2023
75a090e
update per code review
Two-Hearts Nov 28, 2023
bd92473
updated per code review
Two-Hearts Dec 6, 2023
c2f7ef8
fix tests
Two-Hearts Dec 6, 2023
371eaa1
updated function doc
Two-Hearts Dec 6, 2023
b845115
updated function doc
Two-Hearts Dec 6, 2023
3eae658
Merge branch 'notaryproject:main' into plugin
Two-Hearts Dec 11, 2023
002fd51
update
Two-Hearts Dec 11, 2023
14aaaa9
updated per code review
Two-Hearts Dec 11, 2023
b179d54
update
Two-Hearts Dec 11, 2023
c44b738
update
Two-Hearts Dec 11, 2023
c9aa8f1
fix tests
Two-Hearts Dec 11, 2023
6fb5ac6
fix test
Two-Hearts Dec 11, 2023
3f4aa31
add tests
Two-Hearts Dec 12, 2023
dd24043
fix tests
Two-Hearts Dec 12, 2023
1fca1b4
update error msg
Two-Hearts Dec 12, 2023
e8ee2ba
update
Two-Hearts Dec 12, 2023
79ea6b7
fix tests
Two-Hearts Dec 12, 2023
a611ee9
fix tests
Two-Hearts Dec 12, 2023
edd0166
fix tests
Two-Hearts Dec 12, 2023
50ba674
update
Two-Hearts Dec 12, 2023
90bfb95
added clean up before installation
Two-Hearts Dec 12, 2023
c8b1d6e
updated comments
Two-Hearts Dec 12, 2023
f939964
added clean up on failure
Two-Hearts Dec 14, 2023
c7ded4a
update
Two-Hearts Dec 14, 2023
d6eb530
update
Two-Hearts Dec 14, 2023
f623ed9
updated per code review
Two-Hearts Dec 15, 2023
f339741
updated per code review
Two-Hearts Dec 15, 2023
c16b6c3
updated per code review
Two-Hearts Dec 15, 2023
cb32182
updated per code review
Two-Hearts Dec 15, 2023
67d267e
updated per code review
Two-Hearts Dec 18, 2023
d6ab20f
updated error message
Two-Hearts Dec 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion internal/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,60 @@

package file

import "regexp"
import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
)

// IsValidFileName checks if a file name is cross-platform compatible
func IsValidFileName(fileName string) bool {
return regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`).MatchString(fileName)
}

// CopyToDir copies the src file to dst. Existing file will be overwritten.
func CopyToDir(src, dst string) (int64, error) {
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
sourceFileStat, err := os.Stat(src)
if err != nil {
return 0, err
}

if !sourceFileStat.Mode().IsRegular() {
return 0, fmt.Errorf("%s is not a regular file", src)
}

source, err := os.Open(src)
if err != nil {
return 0, err
}
defer source.Close()

if err := os.MkdirAll(dst, 0700); err != nil {
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
return 0, err
}
dstFile := filepath.Join(dst, filepath.Base(src))
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
destination, err := os.Create(dstFile)
if err != nil {
return 0, err
}
defer destination.Close()
err = destination.Chmod(0600)
if err != nil {
return 0, err
}
return io.Copy(destination, source)
}

// 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(fileName string) string {
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
return strings.TrimSuffix(fileName, filepath.Ext(fileName))
}
179 changes: 179 additions & 0 deletions internal/file/file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// 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 file

import (
"bytes"
"os"
"path/filepath"
"runtime"
"testing"
)

func TestCopyToDir(t *testing.T) {
t.Run("copy file", func(t *testing.T) {
tempDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}

destDir := filepath.Join(tempDir, "b")
if _, err := CopyToDir(filename, destDir); err != nil {
t.Fatal(err)
}
})

t.Run("source directory permission error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}

tempDir := t.TempDir()
destDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}

if err := os.Chmod(tempDir, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(tempDir, 0700)

if _, err := CopyToDir(filename, destDir); err == nil {
t.Fatal("should have error")
}
})

t.Run("not a regular file", func(t *testing.T) {
tempDir := t.TempDir()
destDir := t.TempDir()
if _, err := CopyToDir(tempDir, destDir); err == nil {
t.Fatal("should have error")
}
})

t.Run("source file permission error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}

tempDir := t.TempDir()
destDir := t.TempDir()
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}
// forbid reading
if err := os.Chmod(filename, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(filename, 0600)
if _, err := CopyToDir(filename, destDir); err == nil {
t.Fatal("should have error")
}
})

t.Run("dest directory permission error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}

tempDir := t.TempDir()
destTempDir := t.TempDir()
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}
// forbid dest directory operation
if err := os.Chmod(destTempDir, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(destTempDir, 0700)
if _, err := CopyToDir(filename, filepath.Join(destTempDir, "a")); err == nil {
t.Fatal("should have error")
}
})

t.Run("dest directory permission error 2", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}

tempDir := t.TempDir()
destTempDir := t.TempDir()
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}
// forbid writing to destTempDir
if err := os.Chmod(destTempDir, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(destTempDir, 0700)
if _, err := CopyToDir(filename, destTempDir); err == nil {
t.Fatal("should have error")
}
})

t.Run("copy file and check content", func(t *testing.T) {
tempDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
t.Fatal(err)
}

destDir := filepath.Join(tempDir, "b")
if _, err := CopyToDir(filename, destDir); err != nil {
t.Fatal(err)
}
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 validFileContent(t *testing.T, filename string, content []byte) {
b, err := os.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(content, b) {
t.Fatal("file content is not correct")
}
}

func writeFile(path string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}
46 changes: 46 additions & 0 deletions internal/semver/semver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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 provides functions related to semanic version.
// This package is based on "golang.org/x/mod/semver"
package semver
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved

import (
"fmt"
"strings"

"golang.org/x/mod/semver"
)

// ComparePluginVersion validates and compares two plugin semantic versions.
//
// The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
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
}
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
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
}
39 changes: 39 additions & 0 deletions internal/semver/semver_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
49 changes: 49 additions & 0 deletions plugin/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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 "errors"

// ErrNotCompliant is returned by plugin methods when the response is not
// compliant.
var ErrNotCompliant = errors.New("plugin not compliant")

// ErrNotRegularFile is returned when the plugin file is not an regular file.
var ErrNotRegularFile = errors.New("not regular file")

// ErrInstallLowerVersion is returned when installing a plugin with version
// lower than the exisiting plugin version.
type ErrInstallLowerVersion struct {
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
Msg string
}

func (e ErrInstallLowerVersion) Error() string {
if e.Msg != "" {
return e.Msg
}
return "installing plugin with version lower than the existing plugin version"
}

// ErrInstallEqualVersion is returned when installing a plugin with version
// equal to the exisiting plugin version.
type ErrInstallEqualVersion struct {
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
Msg string
}

func (e ErrInstallEqualVersion) Error() string {
if e.Msg != "" {
return e.Msg
}
return "installing plugin with version equal to the existing plugin version"
}
Loading
Loading