Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/wrapper-manager-image.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build amd64 Docker image
name: Build multi-arch Docker image

on:
workflow_dispatch:
Expand All @@ -12,7 +12,7 @@ permissions:
packages: write

concurrency:
group: wrapper-manager-amd64-${{ github.ref }}
group: wrapper-manager-multiarch-${{ github.ref }}
cancel-in-progress: true

env:
Expand Down Expand Up @@ -45,11 +45,11 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}}
type=sha

- name: Build and publish amd64 image
- name: Build and publish multi-arch image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand Down
3 changes: 1 addition & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ COPY proto/go.mod proto/go.sum ./proto/
RUN go mod download

COPY . .
RUN test "$TARGETARCH" = "amd64" && \
CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" \
RUN CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" \
go build -trimpath -ldflags="-s -w" -o /out/wrapper-manager .

FROM debian:13.2
Expand Down
60 changes: 44 additions & 16 deletions wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,32 @@ import (
"strings"
)

const (
wrapperReleaseTag = "wrapper.x86_64.latest"
wrapperAssetName = "Wrapper.x86_64.latest.zip"
)
type wrapperRelease struct {
tag string
assetName string
archivePath string
}

var wrapperReleaseAPIURL = "https://api.github.com/repos/AMDL-Web/wrapper/releases/tags/" + wrapperReleaseTag
var wrapperReleaseAPIBaseURL = "https://api.github.com/repos/AMDL-Web/wrapper/releases/tags/"

func wrapperReleaseForArch(arch string) (wrapperRelease, error) {
switch arch {
case "amd64":
return wrapperRelease{
tag: "wrapper.x86_64.latest",
assetName: "Wrapper.x86_64.latest.zip",
archivePath: "data/wrapper-x86_64.zip",
}, nil
case "arm64":
return wrapperRelease{
tag: "wrapper.arm64.latest",
assetName: "Wrapper.arm64.latest.zip",
archivePath: "data/wrapper-arm64.zip",
}, nil
default:
return wrapperRelease{}, fmt.Errorf("wrapper auto-install only supports amd64 and arm64, current architecture is %s", arch)
}
}

func parseStorefrontID(id string) string {
sfID, err := strconv.Atoi(strings.Split(id, "-")[0])
Expand Down Expand Up @@ -55,17 +75,17 @@ func PrepareWrapper(mirror bool) error {
}

func prepareWrapper(mirror bool, arch string) error {
if arch != "amd64" {
return fmt.Errorf("wrapper auto-install only supports x86_64, current architecture is %s", arch)
release, err := wrapperReleaseForArch(arch)
if err != nil {
return err
}
wrapperZipPath := "data/wrapper-x86_64.zip"
if _, err := os.Stat("data/wrapper/wrapper"); os.IsNotExist(err) {
if _, err := os.Stat(wrapperZipPath); os.IsNotExist(err) {
if err := DownloadWrapperRelease(mirror); err != nil {
if _, err := os.Stat(release.archivePath); os.IsNotExist(err) {
if err := downloadWrapperRelease(mirror, release); err != nil {
return err
}
}
err := unzip.New(wrapperZipPath, "data/wrapper").Extract()
err := unzip.New(release.archivePath, "data/wrapper").Extract()
if err != nil {
return fmt.Errorf("extract wrapper release: %w", err)
}
Expand Down Expand Up @@ -260,7 +280,15 @@ func RemoveWrapperData(id string) {
}

func DownloadWrapperRelease(mirror bool) error {
resp, err := GetHttpClient().Get(wrapperReleaseAPIURL)
release, err := wrapperReleaseForArch(runtime.GOARCH)
if err != nil {
return err
}
return downloadWrapperRelease(mirror, release)
}

func downloadWrapperRelease(mirror bool, expected wrapperRelease) error {
resp, err := GetHttpClient().Get(wrapperReleaseAPIBaseURL + expected.tag)
if err != nil {
return fmt.Errorf("request wrapper release: %w", err)
}
Expand All @@ -281,13 +309,13 @@ func DownloadWrapperRelease(mirror bool) error {

var downloadURL string
for _, asset := range release.Assets {
if asset.Name == wrapperAssetName {
if asset.Name == expected.assetName {
downloadURL = asset.BrowserDownloadURL
break
}
}
if downloadURL == "" {
return fmt.Errorf("wrapper release %s has no %s asset", wrapperReleaseTag, wrapperAssetName)
return fmt.Errorf("wrapper release %s has no %s asset", expected.tag, expected.assetName)
}
if mirror {
downloadURL = strings.Replace(downloadURL, "github.com", "gh-proxy.com/github.com", 1)
Expand All @@ -304,7 +332,7 @@ func DownloadWrapperRelease(mirror bool) error {
if err := os.MkdirAll("data", 0o755); err != nil {
return fmt.Errorf("create data directory: %w", err)
}
temp, err := os.CreateTemp("data", "wrapper-x86_64-*.zip")
temp, err := os.CreateTemp("data", "wrapper-*.zip")
if err != nil {
return fmt.Errorf("create wrapper download: %w", err)
}
Expand All @@ -317,7 +345,7 @@ func DownloadWrapperRelease(mirror bool) error {
if err := temp.Close(); err != nil {
return fmt.Errorf("close wrapper release: %w", err)
}
if err := os.Rename(tempPath, "data/wrapper-x86_64.zip"); err != nil {
if err := os.Rename(tempPath, expected.archivePath); err != nil {
return fmt.Errorf("finalize wrapper release: %w", err)
}
return nil
Expand Down
112 changes: 65 additions & 47 deletions wrapper_download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import (
"testing"
)

func wrapperZip(t *testing.T) []byte {
func wrapperZip(t *testing.T, contents string) []byte {
t.Helper()
var buffer bytes.Buffer
writer := zip.NewWriter(&buffer)
file, err := writer.Create("wrapper")
if err != nil {
t.Fatal(err)
}
if _, err := file.Write([]byte("x86-wrapper")); err != nil {
if _, err := file.Write([]byte(contents)); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
Expand All @@ -29,71 +29,89 @@ func wrapperZip(t *testing.T) []byte {
return buffer.Bytes()
}

func TestPrepareWrapperDownloadsExactX86Asset(t *testing.T) {
archive := wrapperZip(t)
func TestPrepareWrapperDownloadsMatchingArchitectureAsset(t *testing.T) {
archives := map[string][]byte{
"/x86": wrapperZip(t, "x86-wrapper"),
"/arm64": wrapperZip(t, "arm64-wrapper"),
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/release":
fmt.Fprintf(w, `{"assets":[{"name":"Wrapper.arm64.latest.zip","browser_download_url":"%s/arm64"},{"name":"%s","browser_download_url":"%s/x86"}]}`, serverURL(r), wrapperAssetName, serverURL(r))
case "/x86":
if archive, ok := archives[r.URL.Path]; ok {
_, _ = w.Write(archive)
default:
http.NotFound(w, r)
return
}
fmt.Fprintf(w, `{"assets":[{"name":"Wrapper.arm64.latest.zip","browser_download_url":"%s/arm64"},{"name":"Wrapper.x86_64.latest.zip","browser_download_url":"%s/x86"}]}`, serverURL(r), serverURL(r))
}))
defer server.Close()

oldURL := wrapperReleaseAPIURL
wrapperReleaseAPIURL = server.URL + "/release"
defer func() { wrapperReleaseAPIURL = oldURL }()
oldBaseURL := wrapperReleaseAPIBaseURL
wrapperReleaseAPIBaseURL = server.URL + "/"
defer func() { wrapperReleaseAPIBaseURL = oldBaseURL }()

oldWorkingDirectory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(t.TempDir()); err != nil {
t.Fatal(err)
}
defer os.Chdir(oldWorkingDirectory)
for _, test := range []struct {
arch string
wantContents string
wantArchivePath string
}{
{arch: "amd64", wantContents: "x86-wrapper", wantArchivePath: "wrapper-x86_64.zip"},
{arch: "arm64", wantContents: "arm64-wrapper", wantArchivePath: "wrapper-arm64.zip"},
} {
t.Run(test.arch, func(t *testing.T) {
oldWorkingDirectory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if err := os.Chdir(t.TempDir()); err != nil {
t.Fatal(err)
}
defer os.Chdir(oldWorkingDirectory)

if err := prepareWrapper(false, "amd64"); err != nil {
t.Fatal(err)
}
contents, err := os.ReadFile(filepath.Join("data", "wrapper", "wrapper"))
if err != nil {
t.Fatal(err)
}
if string(contents) != "x86-wrapper" {
t.Fatalf("unexpected wrapper contents: %q", contents)
}
info, err := os.Stat(filepath.Join("data", "wrapper", "wrapper"))
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm()&0o111 == 0 {
t.Fatal("downloaded wrapper is not executable")
if err := prepareWrapper(false, test.arch); err != nil {
t.Fatal(err)
}
contents, err := os.ReadFile(filepath.Join("data", "wrapper", "wrapper"))
if err != nil {
t.Fatal(err)
}
if string(contents) != test.wantContents {
t.Fatalf("unexpected wrapper contents: %q", contents)
}
if _, err := os.Stat(filepath.Join("data", test.wantArchivePath)); err != nil {
t.Fatalf("expected architecture archive: %v", err)
}
info, err := os.Stat(filepath.Join("data", "wrapper", "wrapper"))
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm()&0o111 == 0 {
t.Fatal("downloaded wrapper is not executable")
}
})
}
}

func TestDownloadWrapperReleaseRequiresExactAsset(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"assets":[{"name":"Wrapper.arm64.latest.zip","browser_download_url":"https://example.invalid/arm64"}]}`))
_, _ = w.Write([]byte(`{"assets":[{"name":"Wrapper.x86_64.latest.zip","browser_download_url":"https://example.invalid/x86"}]}`))
}))
defer server.Close()

oldURL := wrapperReleaseAPIURL
wrapperReleaseAPIURL = server.URL
defer func() { wrapperReleaseAPIURL = oldURL }()
oldBaseURL := wrapperReleaseAPIBaseURL
wrapperReleaseAPIBaseURL = server.URL + "/"
defer func() { wrapperReleaseAPIBaseURL = oldBaseURL }()

err := DownloadWrapperRelease(false)
if err == nil || !strings.Contains(err.Error(), wrapperAssetName) {
t.Fatalf("expected missing x86 asset error, got %v", err)
release, err := wrapperReleaseForArch("arm64")
if err != nil {
t.Fatal(err)
}
err = downloadWrapperRelease(false, release)
if err == nil || !strings.Contains(err.Error(), release.assetName) {
t.Fatalf("expected missing arm64 asset error, got %v", err)
}
}

func TestPrepareWrapperRejectsNonX86(t *testing.T) {
err := prepareWrapper(false, "arm64")
if err == nil || !strings.Contains(err.Error(), "only supports x86_64") {
func TestPrepareWrapperRejectsUnsupportedArchitecture(t *testing.T) {
err := prepareWrapper(false, "riscv64")
if err == nil || !strings.Contains(err.Error(), "supports amd64 and arm64") {
t.Fatalf("expected unsupported architecture error, got %v", err)
}
}
Expand Down
Loading