Skip to content

Commit 13b6420

Browse files
authored
feat: install / uninstall plugins from URL (#1)
Signed-off-by: Shiwei Zhang <[email protected]>
1 parent 5516199 commit 13b6420

File tree

1 file changed

+208
-0
lines changed

1 file changed

+208
-0
lines changed

cmd/notation/plugin.go

+208
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
package main
22

33
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"errors"
47
"fmt"
8+
"io"
9+
"net/http"
510
"os"
11+
"path/filepath"
12+
"strings"
613
"text/tabwriter"
714

815
"github.com/notaryproject/notation-go/dir"
916
"github.com/notaryproject/notation-go/plugin"
1017
"github.com/notaryproject/notation-go/plugin/proto"
18+
"github.com/opencontainers/go-digest"
1119
"github.com/spf13/cobra"
1220
)
1321

@@ -17,6 +25,8 @@ func pluginCommand() *cobra.Command {
1725
Short: "Manage plugins",
1826
}
1927
cmd.AddCommand(pluginListCommand())
28+
cmd.AddCommand(pluginInstallCommand(nil))
29+
cmd.AddCommand(pluginUninstallCommand(nil))
2030
return cmd
2131
}
2232

@@ -62,3 +72,201 @@ func listPlugins(command *cobra.Command) error {
6272
}
6373
return tw.Flush()
6474
}
75+
76+
type pluginInstallOpts struct {
77+
url string
78+
checksum string
79+
}
80+
81+
func pluginInstallCommand(opts *pluginInstallOpts) *cobra.Command {
82+
if opts == nil {
83+
opts = &pluginInstallOpts{}
84+
}
85+
command := &cobra.Command{
86+
Use: "install [flags]",
87+
Aliases: []string{"add"},
88+
Short: "Install plugin",
89+
Long: `Install plugin
90+
91+
Example - Install Notation plugin from a remote URL:
92+
notation plugin install --checksum sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef https://example.com/notation-plugin-example.tar.gz
93+
`,
94+
Args: cobra.ExactArgs(1),
95+
RunE: func(cmd *cobra.Command, args []string) error {
96+
opts.url = args[0]
97+
return installPlugin(cmd, opts)
98+
},
99+
}
100+
command.Flags().StringVar(&opts.checksum, "checksum", "", "checksum of the plugin")
101+
return command
102+
}
103+
104+
// TODO: should be implemented in notation-go
105+
func installPlugin(command *cobra.Command, opts *pluginInstallOpts) error {
106+
// create a temp directory
107+
tempDir, err := os.MkdirTemp("", "notation-plugin-")
108+
if err != nil {
109+
return err
110+
}
111+
defer os.RemoveAll(tempDir)
112+
113+
// create digester
114+
checksum := opts.checksum
115+
if !strings.Contains(checksum, ":") {
116+
checksum = "sha256:" + checksum
117+
}
118+
packageDigest, err := digest.Parse(checksum)
119+
if err != nil {
120+
return err
121+
}
122+
123+
// download the plugin
124+
// TODO: should limit the size of the plugin
125+
// TODO: should configure the http client
126+
srcPath := filepath.Join(tempDir, "plugin.tar.gz")
127+
if err := downloadFile(opts.url, packageDigest, srcPath); err != nil {
128+
return err
129+
}
130+
131+
// install the plugin
132+
// TODO: should support other plugin types
133+
pluginFilename, pluginFile, err := findPluginExecutable(srcPath)
134+
if err != nil {
135+
return err
136+
}
137+
defer pluginFile.Close()
138+
pluginName := strings.TrimSuffix(pluginFilename, filepath.Ext(pluginFilename))
139+
pluginName = strings.TrimPrefix(pluginName, "notation-")
140+
pluginDir, err := dir.PluginFS().SysPath(pluginName)
141+
if err != nil {
142+
return err
143+
}
144+
if err := os.MkdirAll(pluginDir, 0755); err != nil {
145+
return err
146+
}
147+
148+
// TODO: prompt to overwrite the existing plugin
149+
destPath := filepath.Join(pluginDir, pluginFilename)
150+
destFile, err := os.Create(destPath)
151+
if err != nil {
152+
return err
153+
}
154+
defer destFile.Close() // ensure close
155+
if err := destFile.Chmod(0755); err != nil {
156+
return err
157+
}
158+
if _, err := io.Copy(destFile, pluginFile); err != nil {
159+
return err
160+
}
161+
return destFile.Close()
162+
}
163+
164+
// downloadFile downloads a file from url and verify the checksum
165+
// TODO: add context to cancel the download
166+
func downloadFile(url string, checksum digest.Digest, dest string) error {
167+
verifier := checksum.Verifier()
168+
169+
// download the plugin
170+
// TODO: should limit the size of the plugin
171+
// TODO: should configure the http client
172+
resp, err := http.Get(url)
173+
if err != nil {
174+
return err
175+
}
176+
defer resp.Body.Close()
177+
file, err := os.Create(dest)
178+
if err != nil {
179+
return err
180+
}
181+
defer file.Close() // failsafe close
182+
writer := io.MultiWriter(file, verifier)
183+
if _, err := io.Copy(writer, resp.Body); err != nil {
184+
return err
185+
}
186+
187+
// ensure content is written to the file
188+
if err := file.Close(); err != nil {
189+
return err
190+
}
191+
192+
// verify the checksum
193+
if !verifier.Verified() {
194+
return errors.New("checksum mismatch")
195+
}
196+
197+
return nil
198+
}
199+
200+
func findPluginExecutable(path string) (string, io.ReadCloser, error) {
201+
file, err := os.Open(path)
202+
if err != nil {
203+
return "", nil, err
204+
}
205+
206+
gr, err := gzip.NewReader(file)
207+
if err != nil {
208+
file.Close()
209+
return "", nil, err
210+
}
211+
tr := tar.NewReader(gr)
212+
for {
213+
header, err := tr.Next()
214+
if err != nil {
215+
file.Close()
216+
if err == io.EOF {
217+
return "", nil, errors.New("executable not found")
218+
}
219+
return "", nil, err
220+
}
221+
if header.Typeflag != tar.TypeReg {
222+
continue
223+
}
224+
if strings.HasPrefix(header.Name, "notation-") {
225+
return header.Name, struct {
226+
io.Reader
227+
io.Closer
228+
}{
229+
Reader: tr,
230+
Closer: file,
231+
}, nil
232+
}
233+
}
234+
}
235+
236+
type pluginUninstallOpts struct {
237+
name string
238+
}
239+
240+
func pluginUninstallCommand(opts *pluginUninstallOpts) *cobra.Command {
241+
if opts == nil {
242+
opts = &pluginUninstallOpts{}
243+
}
244+
command := &cobra.Command{
245+
Use: "uninstall [flags]",
246+
Aliases: []string{"remove"},
247+
Short: "Uninstall plugin",
248+
Long: `Uninstall plugin
249+
250+
Example - Uninstall Notation plugin
251+
notation plugin uninstall example-plugin
252+
`,
253+
Args: cobra.ExactArgs(1),
254+
RunE: func(cmd *cobra.Command, args []string) error {
255+
opts.name = args[0]
256+
return uninstallPlugin(cmd, opts)
257+
},
258+
}
259+
return command
260+
}
261+
262+
func uninstallPlugin(command *cobra.Command, opts *pluginUninstallOpts) error {
263+
pluginDir, err := dir.PluginFS().SysPath(opts.name)
264+
if err != nil {
265+
return err
266+
}
267+
if err := os.RemoveAll(pluginDir); err != nil {
268+
return err
269+
}
270+
fmt.Println("Uninstalled plugin:", opts.name)
271+
return nil
272+
}

0 commit comments

Comments
 (0)