Skip to content

Commit 09184fa

Browse files
Add downloaders for uv, go and jq (#3444)
## Why We'll package the CLI repo and binaries for uv, go and jq into a tar.gz archive before uploading it to a serverless cluster for execution. This PR adds the downloader for those tools. A follow-up PR will be sent for actually building the archives. ## Tests Unit tests. The tests have a "dbr_only" build tag added, so they'll not run during normal execution. I plan to run them only from the CI integration run job for DBR.
1 parent 52578a5 commit 09184fa

File tree

5 files changed

+358
-0
lines changed

5 files changed

+358
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//go:build dbr_only
2+
3+
package main
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestUvDownloader(t *testing.T) {
15+
tmpDir := t.TempDir()
16+
17+
for _, arch := range []string{"arm64", "amd64"} {
18+
err := uvDownloader{arch: arch, binDir: tmpDir}.Download()
19+
require.NoError(t, err)
20+
21+
files, err := os.ReadDir(filepath.Join(tmpDir, arch))
22+
require.NoError(t, err)
23+
24+
assert.Equal(t, 1, len(files))
25+
assert.Equal(t, "uv", files[0].Name())
26+
}
27+
}
28+
29+
func TestJqDownloader(t *testing.T) {
30+
tmpDir := t.TempDir()
31+
32+
for _, arch := range []string{"arm64", "amd64"} {
33+
err := jqDownloader{arch: arch, binDir: tmpDir}.Download()
34+
require.NoError(t, err)
35+
36+
files, err := os.ReadDir(filepath.Join(tmpDir, arch))
37+
require.NoError(t, err)
38+
39+
assert.Equal(t, 1, len(files))
40+
assert.Equal(t, "jq", files[0].Name())
41+
}
42+
}
43+
44+
func TestGoDownloader(t *testing.T) {
45+
tmpDir := t.TempDir()
46+
47+
for _, arch := range []string{"arm64", "amd64"} {
48+
err := goDownloader{arch: arch, binDir: tmpDir}.Download()
49+
require.NoError(t, err)
50+
51+
entries, err := os.ReadDir(filepath.Join(tmpDir, arch))
52+
require.NoError(t, err)
53+
54+
assert.Equal(t, 1, len(entries))
55+
assert.Equal(t, "go", entries[0].Name())
56+
assert.True(t, entries[0].IsDir())
57+
58+
binaryPath := filepath.Join(tmpDir, arch, "go", "bin", "go")
59+
_, err = os.Stat(binaryPath)
60+
require.NoError(t, err)
61+
}
62+
}

internal/testarchive/go.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"regexp"
10+
"strings"
11+
)
12+
13+
// Initialize these to prevent linter from complaining about unused types.
14+
// This can be removed once we actually use these downloaders.
15+
var (
16+
_ = goDownloader{}
17+
_ = uvDownloader{}
18+
_ = jqDownloader{}
19+
)
20+
21+
type goDownloader struct {
22+
binDir string
23+
arch string
24+
}
25+
26+
func (g goDownloader) readGoVersionFromMod() (string, error) {
27+
goModPath := filepath.Join("..", "..", "go.mod")
28+
29+
file, err := os.Open(goModPath)
30+
if err != nil {
31+
return "", err
32+
}
33+
defer file.Close()
34+
35+
scanner := bufio.NewScanner(file)
36+
37+
// Get regex version from toolchain go version specified. Eg: toolchain go1.24.6
38+
goVersionRegex := regexp.MustCompile(`^toolchain go(\d+\.\d+\.\d+)$`)
39+
40+
for scanner.Scan() {
41+
line := strings.TrimSpace(scanner.Text())
42+
matches := goVersionRegex.FindStringSubmatch(line)
43+
if matches != nil {
44+
return matches[1], nil
45+
}
46+
}
47+
48+
if err := scanner.Err(); err != nil {
49+
return "", err
50+
}
51+
52+
return "", errors.New("go version not found in go.mod")
53+
}
54+
55+
// Download downloads and extracts Go for Linux
56+
func (g goDownloader) Download() error {
57+
goVersion, err := g.readGoVersionFromMod()
58+
if err != nil {
59+
return fmt.Errorf("failed to read Go version from go.mod: %w", err)
60+
}
61+
62+
// Create the directory for the download if it doesn't exist
63+
dir := filepath.Join(g.binDir, g.arch)
64+
err = os.MkdirAll(dir, 0o755)
65+
if err != nil {
66+
return err
67+
}
68+
69+
// Download the tar archive.
70+
fileName := fmt.Sprintf("go%s.linux-%s.tar.gz", goVersion, g.arch)
71+
url := "https://go.dev/dl/" + fileName
72+
73+
tempFile := filepath.Join(dir, fileName)
74+
err = downloadFile(url, tempFile)
75+
if err != nil {
76+
return err
77+
}
78+
79+
err = extractTarGz(tempFile, dir)
80+
if err != nil {
81+
return err
82+
}
83+
84+
return os.Remove(tempFile)
85+
}

internal/testarchive/jq.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
// jqDownloader handles downloading and extracting jq releases
10+
type jqDownloader struct {
11+
binDir string
12+
arch string
13+
}
14+
15+
// Download downloads and extracts jq for Linux
16+
func (j jqDownloader) Download() error {
17+
// create the directory for the download if it doesn't exist
18+
dir := filepath.Join(j.binDir, j.arch)
19+
err := os.MkdirAll(dir, 0o755)
20+
if err != nil {
21+
return err
22+
}
23+
24+
// Construct the download URL for the latest release
25+
url := "https://github.com/jqlang/jq/releases/latest/download/jq-linux-" + j.arch
26+
27+
binaryPath := filepath.Join(dir, "jq")
28+
if err := downloadFile(url, binaryPath); err != nil {
29+
return err
30+
}
31+
32+
// Make the binary executable
33+
if err := os.Chmod(binaryPath, 0o755); err != nil {
34+
return fmt.Errorf("failed to make jq executable: %w", err)
35+
}
36+
37+
return nil
38+
}

internal/testarchive/utils.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package main
2+
3+
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"path/filepath"
11+
)
12+
13+
func downloadFile(url, outputPath string) error {
14+
resp, err := http.Get(url)
15+
if err != nil {
16+
return fmt.Errorf("failed to download: %w", err)
17+
}
18+
defer resp.Body.Close()
19+
20+
if resp.StatusCode != http.StatusOK {
21+
return fmt.Errorf("download failed with status: %s", resp.Status)
22+
}
23+
24+
outFile, err := os.Create(outputPath)
25+
if err != nil {
26+
return fmt.Errorf("failed to create output file: %w", err)
27+
}
28+
defer outFile.Close()
29+
30+
fmt.Printf("Downloading %s to %s\n", url, outputPath)
31+
_, err = io.Copy(outFile, resp.Body)
32+
if err != nil {
33+
return fmt.Errorf("failed to save file: %w", err)
34+
}
35+
36+
return nil
37+
}
38+
39+
// extractTarGz extracts a tar.gz file to the specified directory
40+
func extractTarGz(archivePath, destDir string) error {
41+
fmt.Printf("Extracting %s to %s\n", archivePath, destDir)
42+
43+
file, err := os.Open(archivePath)
44+
if err != nil {
45+
return fmt.Errorf("failed to open archive: %w", err)
46+
}
47+
defer file.Close()
48+
49+
gzReader, err := gzip.NewReader(file)
50+
if err != nil {
51+
return fmt.Errorf("failed to create gzip reader: %w", err)
52+
}
53+
defer gzReader.Close()
54+
55+
tarReader := tar.NewReader(gzReader)
56+
57+
for {
58+
header, err := tarReader.Next()
59+
if err == io.EOF {
60+
break
61+
}
62+
if err != nil {
63+
return fmt.Errorf("failed to read tar header: %w", err)
64+
}
65+
66+
targetPath := filepath.Join(destDir, header.Name)
67+
68+
switch header.Typeflag {
69+
case tar.TypeDir:
70+
if err := os.MkdirAll(targetPath, 0o755); err != nil {
71+
return fmt.Errorf("failed to create directory %s: %w", targetPath, err)
72+
}
73+
case tar.TypeReg:
74+
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
75+
return fmt.Errorf("failed to create parent directory: %w", err)
76+
}
77+
78+
outFile, err := os.Create(targetPath)
79+
if err != nil {
80+
return fmt.Errorf("failed to create file %s: %w", targetPath, err)
81+
}
82+
83+
_, err = io.Copy(outFile, tarReader)
84+
outFile.Close()
85+
if err != nil {
86+
return fmt.Errorf("failed to extract file %s: %w", targetPath, err)
87+
}
88+
89+
// Set file permissions
90+
if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil {
91+
fmt.Printf("Warning: failed to set permissions for %s: %v\n", targetPath, err)
92+
}
93+
}
94+
}
95+
96+
return nil
97+
}

internal/testarchive/uv.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
// uvDownloader handles downloading and extracting UV releases
10+
type uvDownloader struct {
11+
binDir string
12+
arch string
13+
}
14+
15+
// uvDownloader creates a new UV downloader
16+
17+
// mapArchitecture maps our architecture names to UV's naming convention
18+
func (u uvDownloader) mapArchitecture(arch string) (string, error) {
19+
switch arch {
20+
case "arm64":
21+
return "aarch64", nil
22+
case "amd64":
23+
return "x86_64", nil
24+
default:
25+
return "", fmt.Errorf("unsupported architecture: %s (supported: arm64, amd64)", arch)
26+
}
27+
}
28+
29+
// Download downloads and extracts UV for Linux
30+
func (u uvDownloader) Download() error {
31+
// Map architecture names to UV's naming convention
32+
uvArch, err := u.mapArchitecture(u.arch)
33+
if err != nil {
34+
return err
35+
}
36+
37+
// create the directory for the download if it doesn't exist
38+
dir := filepath.Join(u.binDir, u.arch)
39+
err = os.MkdirAll(dir, 0o755)
40+
if err != nil {
41+
return err
42+
}
43+
44+
// Construct the download URL for the latest release
45+
uvTarName := fmt.Sprintf("uv-%s-unknown-linux-gnu", uvArch)
46+
url := fmt.Sprintf("https://github.com/astral-sh/uv/releases/latest/download/%s.tar.gz", uvTarName)
47+
48+
// Download the file using shared utility
49+
tempFile := filepath.Join(dir, "uv.tar.gz")
50+
if err := downloadFile(url, tempFile); err != nil {
51+
return err
52+
}
53+
54+
// Extract the archive to the directory
55+
if err := extractTarGz(tempFile, dir); err != nil {
56+
return err
57+
}
58+
59+
err = os.Remove(tempFile)
60+
if err != nil {
61+
return err
62+
}
63+
64+
// The uv binary is extracted into a directory called something like
65+
// uv-aarch64-unknown-linux-gnu. We remove that additional directory here
66+
// and move the uv binary one level above to keep the directory structure clean.
67+
err = os.Rename(filepath.Join(dir, uvTarName, "uv"), filepath.Join(dir, "uv"))
68+
if err != nil {
69+
return err
70+
}
71+
err = os.RemoveAll(filepath.Join(dir, uvTarName))
72+
if err != nil {
73+
return err
74+
}
75+
return nil
76+
}

0 commit comments

Comments
 (0)