|
| 1 | +package integration_test |
| 2 | + |
| 3 | +import ( |
| 4 | + "archive/zip" |
| 5 | + "fmt" |
| 6 | + "io" |
| 7 | + "net/http" |
| 8 | + "os" |
| 9 | + "path/filepath" |
| 10 | + "strings" |
| 11 | + "time" |
| 12 | +) |
| 13 | + |
| 14 | +const ( |
| 15 | + javaTestAppsReleaseTag = "v1.0.0" |
| 16 | + javaTestAppsBaseURL = "https://github.com/cloudfoundry/java-test-applications/releases/download/" + javaTestAppsReleaseTag |
| 17 | + sb3JarName = "java-main-application-boot3-1.0.0.jar" |
| 18 | + sb4JarName = "java-main-application-1.0.0.jar" |
| 19 | +) |
| 20 | + |
| 21 | +// downloadJavaTestAppsJars downloads the pinned SB3 and SB4 fat jars. |
| 22 | +// For Docker-mode tests, jars are extracted (exploded) into fixture directories |
| 23 | +// to simulate what real CF staging does: CF treats the pushed artifact as a zip |
| 24 | +// and extracts it before running the buildpack, so BOOT-INF/ and META-INF/ |
| 25 | +// appear as flat files on disk. |
| 26 | +// For CF-mode tests, the raw jar is returned as-is — `cf push -p` handles |
| 27 | +// zip extraction natively. |
| 28 | +// |
| 29 | +// Pre-exploding is the correct pattern for Docker-mode tests: switchblade's |
| 30 | +// Docker mode archives the fixture directory as-is (TGZArchiver), while CF mode |
| 31 | +// delegates to `cf push -p` which handles zip extraction natively. |
| 32 | +// See https://github.com/cloudfoundry/switchblade/issues/134 for details. |
| 33 | +// |
| 34 | +// Returns (sb3FixturePath, sb4FixturePath, cleanup func, error). |
| 35 | +func downloadJavaTestAppsJars(platform string) (string, string, func(), error) { |
| 36 | + dir, err := os.MkdirTemp("", "java-test-apps-*") |
| 37 | + if err != nil { |
| 38 | + return "", "", nil, fmt.Errorf("create temp dir: %w", err) |
| 39 | + } |
| 40 | + |
| 41 | + cleanup := func() { os.RemoveAll(dir) } |
| 42 | + |
| 43 | + type jarEntry struct { |
| 44 | + jarName string |
| 45 | + path string // set after download/extract |
| 46 | + } |
| 47 | + entries := []jarEntry{ |
| 48 | + {jarName: sb3JarName}, |
| 49 | + {jarName: sb4JarName}, |
| 50 | + } |
| 51 | + |
| 52 | + for i := range entries { |
| 53 | + jarPath := filepath.Join(dir, entries[i].jarName) |
| 54 | + if err := downloadFile(javaTestAppsBaseURL+"/"+entries[i].jarName, jarPath); err != nil { |
| 55 | + cleanup() |
| 56 | + return "", "", nil, fmt.Errorf("download %s: %w", entries[i].jarName, err) |
| 57 | + } |
| 58 | + |
| 59 | + if platform == "docker" { |
| 60 | + // Docker mode: explode jar into directory (simulates CF staging zip extraction). |
| 61 | + // Switchblade Docker archives the fixture dir as-is via TGZArchiver. |
| 62 | + explodedDir := filepath.Join(dir, fmt.Sprintf("exploded-%d", i)) |
| 63 | + if err := extractZip(jarPath, explodedDir); err != nil { |
| 64 | + cleanup() |
| 65 | + return "", "", nil, fmt.Errorf("extract %s: %w", entries[i].jarName, err) |
| 66 | + } |
| 67 | + _ = os.Remove(jarPath) |
| 68 | + entries[i].path = explodedDir |
| 69 | + } else { |
| 70 | + // CF mode: return raw jar path — `cf push -p` handles zip extraction natively. |
| 71 | + entries[i].path = jarPath |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + return entries[0].path, entries[1].path, cleanup, nil |
| 76 | +} |
| 77 | + |
| 78 | +// extractZip extracts a zip/jar file into destDir, replicating the flat-file |
| 79 | +// layout that CF staging creates when it unpacks the pushed artifact. |
| 80 | +func extractZip(src, destDir string) error { |
| 81 | + r, err := zip.OpenReader(src) |
| 82 | + if err != nil { |
| 83 | + return fmt.Errorf("open zip: %w", err) |
| 84 | + } |
| 85 | + defer r.Close() |
| 86 | + |
| 87 | + for _, f := range r.File { |
| 88 | + target := filepath.Join(destDir, f.Name) |
| 89 | + |
| 90 | + // Guard against zip-slip |
| 91 | + if !strings.HasPrefix(filepath.Clean(target)+string(os.PathSeparator), |
| 92 | + filepath.Clean(destDir)+string(os.PathSeparator)) { |
| 93 | + return fmt.Errorf("zip entry %q escapes destination", f.Name) |
| 94 | + } |
| 95 | + |
| 96 | + if f.FileInfo().IsDir() { |
| 97 | + if err := os.MkdirAll(target, 0755); err != nil { |
| 98 | + return err |
| 99 | + } |
| 100 | + continue |
| 101 | + } |
| 102 | + |
| 103 | + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { |
| 104 | + return err |
| 105 | + } |
| 106 | + |
| 107 | + out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode()) |
| 108 | + if err != nil { |
| 109 | + return err |
| 110 | + } |
| 111 | + |
| 112 | + rc, err := f.Open() |
| 113 | + if err != nil { |
| 114 | + out.Close() |
| 115 | + return err |
| 116 | + } |
| 117 | + |
| 118 | + _, copyErr := io.Copy(out, rc) |
| 119 | + rc.Close() |
| 120 | + out.Close() |
| 121 | + if copyErr != nil { |
| 122 | + return copyErr |
| 123 | + } |
| 124 | + } |
| 125 | + return nil |
| 126 | +} |
| 127 | + |
| 128 | +func downloadFile(url, dest string) error { |
| 129 | + client := &http.Client{Timeout: 2 * time.Minute} |
| 130 | + resp, err := client.Get(url) //nolint:noctx |
| 131 | + if err != nil { |
| 132 | + return err |
| 133 | + } |
| 134 | + defer resp.Body.Close() |
| 135 | + |
| 136 | + if resp.StatusCode != http.StatusOK { |
| 137 | + return fmt.Errorf("HTTP %d for %s", resp.StatusCode, url) |
| 138 | + } |
| 139 | + |
| 140 | + f, err := os.Create(dest) |
| 141 | + if err != nil { |
| 142 | + return err |
| 143 | + } |
| 144 | + defer f.Close() |
| 145 | + |
| 146 | + _, err = io.Copy(f, resp.Body) |
| 147 | + return err |
| 148 | +} |
0 commit comments