1
1
package main
2
2
3
3
import (
4
+ "archive/tar"
5
+ "compress/gzip"
6
+ "errors"
4
7
"fmt"
8
+ "io"
9
+ "net/http"
5
10
"os"
11
+ "path/filepath"
12
+ "strings"
6
13
"text/tabwriter"
7
14
8
15
"github.com/notaryproject/notation-go/dir"
9
16
"github.com/notaryproject/notation-go/plugin"
10
17
"github.com/notaryproject/notation-go/plugin/proto"
18
+ "github.com/opencontainers/go-digest"
11
19
"github.com/spf13/cobra"
12
20
)
13
21
@@ -17,6 +25,8 @@ func pluginCommand() *cobra.Command {
17
25
Short : "Manage plugins" ,
18
26
}
19
27
cmd .AddCommand (pluginListCommand ())
28
+ cmd .AddCommand (pluginInstallCommand (nil ))
29
+ cmd .AddCommand (pluginUninstallCommand (nil ))
20
30
return cmd
21
31
}
22
32
@@ -62,3 +72,201 @@ func listPlugins(command *cobra.Command) error {
62
72
}
63
73
return tw .Flush ()
64
74
}
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