garm/workers/cache/gitea_tools.go
Gabriel Adrian Samfira 76f538ffb3 Switch to uncompressed tools for gitea
Use uncompressed tools for gitea. Gitea compresses using .xz, including for
Windows, which does not have a native, built-in tool to uncompress that
format.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2025-10-05 11:31:41 +03:00

225 lines
6.6 KiB
Go

// Copyright 2025 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package cache
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"time"
"golang.org/x/mod/semver"
commonParams "github.com/cloudbase/garm-provider-common/params"
"github.com/cloudbase/garm/util/appdefaults"
)
var githubArchMapping = map[string]string{
"x86_64": "x64",
"amd64": "x64",
"armv7l": "arm",
"aarch64": "arm64",
"x64": "x64",
"arm": "arm",
"arm64": "arm64",
}
var nightlyActRunner = GiteaEntityTool{
TagName: "nightly",
Name: "nightly",
TarballURL: "https://gitea.com/gitea/act_runner/archive/main.tar.gz",
Assets: []GiteaToolsAssets{
{
Name: "act_runner-nightly-linux-amd64.xz",
DownloadURL: "https://dl.gitea.com/act_runner/nightly/act_runner-nightly-linux-amd64",
},
{
Name: "act_runner-nightly-linux-arm64.xz",
DownloadURL: "https://dl.gitea.com/act_runner/nightly/act_runner-nightly-linux-arm64",
},
{
Name: "act_runner-nightly-windows-amd64.exe.xz",
DownloadURL: "https://dl.gitea.com/act_runner/nightly/act_runner-nightly-windows-amd64.exe",
},
},
}
type GiteaToolsAssets struct {
ID uint `json:"id"`
Name string `json:"name"`
Size uint `json:"size"`
DownloadCount uint `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
UUID string `json:"uuid"`
DownloadURL string `json:"browser_download_url"`
}
func (g GiteaToolsAssets) GetOS() (*string, error) {
if g.Name == "" {
return nil, fmt.Errorf("gitea tools name is empty")
}
parts := strings.SplitN(g.Name, "-", 4)
if len(parts) != 4 {
return nil, fmt.Errorf("could not parse asset name")
}
os := parts[2]
return &os, nil
}
func (g GiteaToolsAssets) GetArch() (*string, error) {
if g.Name == "" {
return nil, fmt.Errorf("gitea tools name is empty")
}
parts := strings.SplitN(g.Name, "-", 4)
if len(parts) != 4 {
return nil, fmt.Errorf("could not parse asset name")
}
archParts := strings.SplitN(parts[3], ".", 2)
if len(archParts) == 0 {
return nil, fmt.Errorf("unexpected asset name format")
}
arch := githubArchMapping[archParts[0]]
if arch == "" {
return nil, fmt.Errorf("could not find arch for %s", archParts[0])
}
return &arch, nil
}
type GiteaEntityTool struct {
// TagName is the semver version of the release.
TagName string `json:"tag_name"`
Name string `json:"name"`
TarballURL string `json:"tarball_url"`
Assets []GiteaToolsAssets `json:"assets"`
}
type GiteaEntityTools []GiteaEntityTool
func (g GiteaEntityTools) GetLatestVersion() string {
if len(g) == 0 {
return ""
}
return g[0].TagName
}
func (g GiteaEntityTools) MinimumVersion() (GiteaEntityTool, bool) {
if len(g) == 0 {
return GiteaEntityTool{}, false
}
for _, tool := range g {
if semver.Compare(tool.TagName, appdefaults.GiteaRunnerMinimumVersion) >= 0 {
return tool, true
}
}
return GiteaEntityTool{}, false
}
func getReleasesFromURL(ctx context.Context, metadataURL string) (GiteaEntityTool, error) {
if metadataURL == "" {
metadataURL = appdefaults.GiteaRunnerReleasesURL
}
// We don't return the result to the user. We get the data and attempt to unmarshal
// the result as a specific json. If that fails, we error out. The value is set by the
// admin/user of GARM after authentication. If they have admin rights to GARM, they most
// likely have admin rights to the machine running GARM, in which case, they can just
// GET the metadataURL manually from that server.
resp, err := http.Get(metadataURL) // nolint
if err != nil {
return GiteaEntityTool{}, fmt.Errorf("failed to fetch URL %s: %w", metadataURL, err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return GiteaEntityTool{}, fmt.Errorf("failed to read response from URL %s: %w", metadataURL, err)
}
var tools GiteaEntityTools
err = json.Unmarshal(data, &tools)
if err != nil {
return GiteaEntityTool{}, fmt.Errorf("failed to unmarshal response from URL %s: %w", metadataURL, err)
}
if len(tools) == 0 {
return GiteaEntityTool{}, fmt.Errorf("no tools found from URL %s", metadataURL)
}
latest, ok := tools.MinimumVersion()
if !ok {
slog.InfoContext(ctx, "failed to find tools, falling back to nightly")
latest = nightlyActRunner
}
return latest, nil
}
func getTools(ctx context.Context, metadataURL string, useInternal bool) ([]commonParams.RunnerApplicationDownload, error) {
if metadataURL == "" {
metadataURL = appdefaults.GiteaRunnerReleasesURL
}
var latest GiteaEntityTool
var err error
if useInternal {
latest = nightlyActRunner
} else {
latest, err = getReleasesFromURL(ctx, metadataURL)
if err != nil {
slog.ErrorContext(ctx, "failed to get tools from metadata URL", "error", err)
if metadataURL != appdefaults.GiteaRunnerReleasesURL {
slog.InfoContext(ctx, "attempting to get tools from default upstream", "tools_url", appdefaults.GiteaRunnerReleasesURL)
latest, err = getReleasesFromURL(ctx, appdefaults.GiteaRunnerReleasesURL)
}
}
}
if err != nil {
return nil, fmt.Errorf("failed to get tools: %w", err)
}
ret := []commonParams.RunnerApplicationDownload{}
for _, asset := range latest.Assets {
arch, err := asset.GetArch()
if err != nil {
slog.InfoContext(ctx, "ignoring unrecognized tools arch", "tool", asset.Name)
continue
}
os, err := asset.GetOS()
if err != nil {
slog.InfoContext(ctx, "ignoring unrecognized tools os", "tool", asset.Name)
continue
}
if strings.HasSuffix(asset.DownloadURL, ".xz") || strings.HasSuffix(asset.DownloadURL, ".sha256") {
// filter out compressed files and sha256 sums. Windows does not have any way to uncompress
// .xz by default.
continue
}
slog.DebugContext(ctx, "found valid tools", "download_url", asset.DownloadURL, "os", os, "arch", arch, "file_name", asset.Name)
ret = append(ret, commonParams.RunnerApplicationDownload{
OS: os,
Architecture: arch,
DownloadURL: &asset.DownloadURL,
Filename: &asset.Name,
})
}
return ret, nil
}