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