1/* 2Copyright The Helm Authors. 3Licensed under the Apache License, Version 2.0 (the "License"); 4you may not use this file except in compliance with the License. 5You may obtain a copy of the License at 6 7http://www.apache.org/licenses/LICENSE-2.0 8 9Unless required by applicable law or agreed to in writing, software 10distributed under the License is distributed on an "AS IS" BASIS, 11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12See the License for the specific language governing permissions and 13limitations under the License. 14*/ 15 16package installer // import "helm.sh/helm/v3/pkg/plugin/installer" 17 18import ( 19 "archive/tar" 20 "bytes" 21 "compress/gzip" 22 "encoding/base64" 23 "fmt" 24 "io/ioutil" 25 "net/http" 26 "net/http/httptest" 27 "os" 28 "path/filepath" 29 "strings" 30 "syscall" 31 "testing" 32 33 "github.com/pkg/errors" 34 35 "helm.sh/helm/v3/internal/test/ensure" 36 "helm.sh/helm/v3/pkg/getter" 37 "helm.sh/helm/v3/pkg/helmpath" 38) 39 40var _ Installer = new(HTTPInstaller) 41 42// Fake http client 43type TestHTTPGetter struct { 44 MockResponse *bytes.Buffer 45 MockError error 46} 47 48func (t *TestHTTPGetter) Get(href string, _ ...getter.Option) (*bytes.Buffer, error) { 49 return t.MockResponse, t.MockError 50} 51 52// Fake plugin tarball data 53var fakePluginB64 = "H4sIAKRj51kAA+3UX0vCUBgGcC9jn+Iwuk3Peza3GeyiUlJQkcogCOzgli7dJm4TvYk+a5+k479UqquUCJ/fLs549sLO2TnvWnJa9aXnjwujYdYLovxMhsPcfnHOLdNkOXthM/IVQQYjg2yyLLJ4kXGhLp5j0z3P41tZksqxmspL3B/O+j/XtZu1y8rdYzkOZRCxduKPk53ny6Wwz/GfIIf1As8lxzGJSmoHNLJZphKHG4YpTCE0wVk3DULfpSJ3DMMqkj3P5JfMYLdX1Vr9Ie/5E5cstcdC8K04iGLX5HaJuKpWL17F0TCIBi5pf/0pjtLhun5j3f9v6r7wfnI/H0eNp9d1/5P6Gez0vzo7wsoxfrAZbTny/o9k6J8z/VkO/LPlWdC1iVpbEEcq5nmeJ13LEtmbV0k2r2PrOs9PuuNglC5rL1Y5S/syXRQmutaNw1BGnnp8Wq3UG51WvX1da3bKtZtCN/R09DwAAAAAAAAAAAAAAAAAAADAb30AoMczDwAoAAA=" 54 55func TestStripName(t *testing.T) { 56 if stripPluginName("fake-plugin-0.0.1.tar.gz") != "fake-plugin" { 57 t.Errorf("name does not match expected value") 58 } 59 if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" { 60 t.Errorf("name does not match expected value") 61 } 62 if stripPluginName("fake-plugin.tgz") != "fake-plugin" { 63 t.Errorf("name does not match expected value") 64 } 65 if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" { 66 t.Errorf("name does not match expected value") 67 } 68} 69 70func mockArchiveServer() *httptest.Server { 71 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 72 if !strings.HasSuffix(r.URL.Path, ".tar.gz") { 73 w.Header().Add("Content-Type", "text/html") 74 fmt.Fprintln(w, "broken") 75 return 76 } 77 w.Header().Add("Content-Type", "application/gzip") 78 fmt.Fprintln(w, "test") 79 })) 80} 81 82func TestHTTPInstaller(t *testing.T) { 83 defer ensure.HelmHome(t)() 84 85 srv := mockArchiveServer() 86 defer srv.Close() 87 source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" 88 89 if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { 90 t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) 91 } 92 93 i, err := NewForSource(source, "0.0.1") 94 if err != nil { 95 t.Fatalf("unexpected error: %s", err) 96 } 97 98 // ensure a HTTPInstaller was returned 99 httpInstaller, ok := i.(*HTTPInstaller) 100 if !ok { 101 t.Fatal("expected a HTTPInstaller") 102 } 103 104 // inject fake http client responding with minimal plugin tarball 105 mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64) 106 if err != nil { 107 t.Fatalf("Could not decode fake tgz plugin: %s", err) 108 } 109 110 httpInstaller.getter = &TestHTTPGetter{ 111 MockResponse: bytes.NewBuffer(mockTgz), 112 } 113 114 // install the plugin 115 if err := Install(i); err != nil { 116 t.Fatal(err) 117 } 118 if i.Path() != helmpath.DataPath("plugins", "fake-plugin") { 119 t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) 120 } 121 122 // Install again to test plugin exists error 123 if err := Install(i); err == nil { 124 t.Fatal("expected error for plugin exists, got none") 125 } else if err.Error() != "plugin already exists" { 126 t.Fatalf("expected error for plugin exists, got (%v)", err) 127 } 128 129} 130 131func TestHTTPInstallerNonExistentVersion(t *testing.T) { 132 defer ensure.HelmHome(t)() 133 srv := mockArchiveServer() 134 defer srv.Close() 135 source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" 136 137 if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { 138 t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) 139 } 140 141 i, err := NewForSource(source, "0.0.2") 142 if err != nil { 143 t.Fatalf("unexpected error: %s", err) 144 } 145 146 // ensure a HTTPInstaller was returned 147 httpInstaller, ok := i.(*HTTPInstaller) 148 if !ok { 149 t.Fatal("expected a HTTPInstaller") 150 } 151 152 // inject fake http client responding with error 153 httpInstaller.getter = &TestHTTPGetter{ 154 MockError: errors.Errorf("failed to download plugin for some reason"), 155 } 156 157 // attempt to install the plugin 158 if err := Install(i); err == nil { 159 t.Fatal("expected error from http client") 160 } 161 162} 163 164func TestHTTPInstallerUpdate(t *testing.T) { 165 srv := mockArchiveServer() 166 defer srv.Close() 167 source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" 168 defer ensure.HelmHome(t)() 169 170 if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { 171 t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) 172 } 173 174 i, err := NewForSource(source, "0.0.1") 175 if err != nil { 176 t.Fatalf("unexpected error: %s", err) 177 } 178 179 // ensure a HTTPInstaller was returned 180 httpInstaller, ok := i.(*HTTPInstaller) 181 if !ok { 182 t.Fatal("expected a HTTPInstaller") 183 } 184 185 // inject fake http client responding with minimal plugin tarball 186 mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64) 187 if err != nil { 188 t.Fatalf("Could not decode fake tgz plugin: %s", err) 189 } 190 191 httpInstaller.getter = &TestHTTPGetter{ 192 MockResponse: bytes.NewBuffer(mockTgz), 193 } 194 195 // install the plugin before updating 196 if err := Install(i); err != nil { 197 t.Fatal(err) 198 } 199 if i.Path() != helmpath.DataPath("plugins", "fake-plugin") { 200 t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) 201 } 202 203 // Update plugin, should fail because it is not implemented 204 if err := Update(i); err == nil { 205 t.Fatal("update method not implemented for http installer") 206 } 207} 208 209func TestExtract(t *testing.T) { 210 source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz" 211 212 tempDir, err := ioutil.TempDir("", "") 213 if err != nil { 214 t.Fatal(err) 215 } 216 defer os.RemoveAll(tempDir) 217 218 // Set the umask to default open permissions so we can actually test 219 oldmask := syscall.Umask(0000) 220 defer func() { 221 syscall.Umask(oldmask) 222 }() 223 224 // Write a tarball to a buffer for us to extract 225 var tarbuf bytes.Buffer 226 tw := tar.NewWriter(&tarbuf) 227 var files = []struct { 228 Name, Body string 229 Mode int64 230 }{ 231 {"plugin.yaml", "plugin metadata", 0600}, 232 {"README.md", "some text", 0777}, 233 } 234 for _, file := range files { 235 hdr := &tar.Header{ 236 Name: file.Name, 237 Typeflag: tar.TypeReg, 238 Mode: file.Mode, 239 Size: int64(len(file.Body)), 240 } 241 if err := tw.WriteHeader(hdr); err != nil { 242 t.Fatal(err) 243 } 244 if _, err := tw.Write([]byte(file.Body)); err != nil { 245 t.Fatal(err) 246 } 247 } 248 249 // Add pax global headers. This should be ignored. 250 // Note the PAX header that isn't global cannot be written using WriteHeader. 251 // Details are in the internal Go function for the tar packaged named 252 // allowedFormats. For a TypeXHeader it will return a message stating 253 // "cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers" 254 if err := tw.WriteHeader(&tar.Header{ 255 Name: "pax_global_header", 256 Typeflag: tar.TypeXGlobalHeader, 257 }); err != nil { 258 t.Fatal(err) 259 } 260 261 if err := tw.Close(); err != nil { 262 t.Fatal(err) 263 } 264 265 var buf bytes.Buffer 266 gz := gzip.NewWriter(&buf) 267 if _, err := gz.Write(tarbuf.Bytes()); err != nil { 268 t.Fatal(err) 269 } 270 gz.Close() 271 // END tarball creation 272 273 extractor, err := NewExtractor(source) 274 if err != nil { 275 t.Fatal(err) 276 } 277 278 if err = extractor.Extract(&buf, tempDir); err != nil { 279 t.Fatalf("Did not expect error but got error: %v", err) 280 } 281 282 pluginYAMLFullPath := filepath.Join(tempDir, "plugin.yaml") 283 if info, err := os.Stat(pluginYAMLFullPath); err != nil { 284 if os.IsNotExist(err) { 285 t.Fatalf("Expected %s to exist but doesn't", pluginYAMLFullPath) 286 } 287 t.Fatal(err) 288 } else if info.Mode().Perm() != 0600 { 289 t.Fatalf("Expected %s to have 0600 mode it but has %o", pluginYAMLFullPath, info.Mode().Perm()) 290 } 291 292 readmeFullPath := filepath.Join(tempDir, "README.md") 293 if info, err := os.Stat(readmeFullPath); err != nil { 294 if os.IsNotExist(err) { 295 t.Fatalf("Expected %s to exist but doesn't", readmeFullPath) 296 } 297 t.Fatal(err) 298 } else if info.Mode().Perm() != 0777 { 299 t.Fatalf("Expected %s to have 0777 mode it but has %o", readmeFullPath, info.Mode().Perm()) 300 } 301 302} 303 304func TestCleanJoin(t *testing.T) { 305 for i, fixture := range []struct { 306 path string 307 expect string 308 expectError bool 309 }{ 310 {"foo/bar.txt", "/tmp/foo/bar.txt", false}, 311 {"/foo/bar.txt", "", true}, 312 {"./foo/bar.txt", "/tmp/foo/bar.txt", false}, 313 {"./././././foo/bar.txt", "/tmp/foo/bar.txt", false}, 314 {"../../../../foo/bar.txt", "", true}, 315 {"foo/../../../../bar.txt", "", true}, 316 {"c:/foo/bar.txt", "/tmp/c:/foo/bar.txt", true}, 317 {"foo\\bar.txt", "/tmp/foo/bar.txt", false}, 318 {"c:\\foo\\bar.txt", "", true}, 319 } { 320 out, err := cleanJoin("/tmp", fixture.path) 321 if err != nil { 322 if !fixture.expectError { 323 t.Errorf("Test %d: Path was not cleaned: %s", i, err) 324 } 325 continue 326 } 327 if fixture.expect != out { 328 t.Errorf("Test %d: Expected %q but got %q", i, fixture.expect, out) 329 } 330 } 331 332} 333 334func TestMediaTypeToExtension(t *testing.T) { 335 336 for mt, shouldPass := range map[string]bool{ 337 "": false, 338 "application/gzip": true, 339 "application/x-gzip": true, 340 "application/x-tgz": true, 341 "application/x-gtar": true, 342 "application/json": false, 343 } { 344 ext, ok := mediaTypeToExtension(mt) 345 if ok != shouldPass { 346 t.Errorf("Media type %q failed test", mt) 347 } 348 if shouldPass && ext == "" { 349 t.Errorf("Expected an extension but got empty string") 350 } 351 if !shouldPass && len(ext) != 0 { 352 t.Error("Expected extension to be empty for unrecognized type") 353 } 354 } 355} 356