garm/util/garm_agent.go
Gabriel Adrian Samfira def4b4aaf1 Handle garm-agent tools upload/sync
This change adds the ability to manage garm-agent tools downloads. Users
can:

* Set an upstream releases page (github releases api)
* Enable sync from upstream. In this case, GARM will automatically download
  garm-agent tools from the releases page and save them in the internal
  object store
* Manually upload tools. Manually uploaded tools for an OS/arch combination
  will never be overwritten by auto-sync. Usrs will need to delete manually
  uploaded tools to enable sync for that os/arch release.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2026-02-08 00:27:47 +02:00

128 lines
4 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 util
import (
"encoding/json"
"fmt"
"strings"
"time"
commonParams "github.com/cloudbase/garm-provider-common/params"
"github.com/cloudbase/garm/params"
)
// GitHubReleaseAsset represents an asset from a GitHub release
type GitHubReleaseAsset struct {
ID uint `json:"id"`
Name string `json:"name"`
Size uint `json:"size"`
DownloadCount uint `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
Digest string `json:"digest"`
DownloadURL string `json:"browser_download_url"`
}
// GitHubRelease represents a GitHub release
type GitHubRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
TarballURL string `json:"tarball_url"`
Assets []GitHubReleaseAsset `json:"assets"`
}
// GitHubReleases represents an array of GitHub releases
type GitHubReleases []GitHubRelease
// ParseGARMAgentAssetName parses a garm-agent asset name to extract OS type and architecture
func ParseGARMAgentAssetName(name string) (osType, osArch string, err error) {
// Skip checksum files
if strings.HasSuffix(name, ".sha256") || strings.HasSuffix(name, ".md5") {
return "", "", fmt.Errorf("checksum file, skipping")
}
// Remove .exe extension if present
name = strings.TrimSuffix(name, ".exe")
// Expected format: garm-agent-{os}-{arch}[-{version}]
const prefix = "garm-agent-"
if len(name) < len(prefix) || !strings.HasPrefix(name, prefix) {
return "", "", fmt.Errorf("invalid asset name format: %s (expected to start with %s)", name, prefix)
}
// Split the remainder after "garm-agent-"
remainder := name[len(prefix):]
parts := strings.Split(remainder, "-")
if len(parts) < 2 {
return "", "", fmt.Errorf("invalid asset name format: %s (expected {os}-{arch})", name)
}
osType = parts[0]
osArch = parts[1]
return osType, osArch, nil
}
// ParseToolsFromRelease parses cached release data and extracts GARM agent tool information
func ParseToolsFromRelease(releaseData []byte) (map[string]params.GARMAgentTool, error) {
// Try to unmarshal as an array first
var releases GitHubReleases
var release GitHubRelease
err := json.Unmarshal(releaseData, &releases)
if err == nil && len(releases) > 0 {
// Successfully parsed as array with at least one release
release = releases[0]
} else {
// Try as a single release object
if err := json.Unmarshal(releaseData, &release); err != nil {
return nil, fmt.Errorf("failed to unmarshal release data: %w", err)
}
// Validate it has required fields
if release.TagName == "" {
return nil, fmt.Errorf("invalid release format: missing tag_name")
}
}
tools := make(map[string]params.GARMAgentTool)
for _, asset := range release.Assets {
// Skip checksum files
if strings.HasSuffix(asset.Name, ".sha256") || strings.HasSuffix(asset.Name, ".md5") {
continue
}
// Parse asset name
osType, osArch, err := ParseGARMAgentAssetName(asset.Name)
if err != nil {
continue
}
// Create key
key := osType + "/" + osArch
// Create tool
tools[key] = params.GARMAgentTool{
Name: asset.Name,
Description: fmt.Sprintf("GARM Agent %s for %s/%s", release.TagName, osType, osArch),
Size: int64(asset.Size),
Version: release.TagName,
OSType: commonParams.OSType(osType),
OSArch: commonParams.OSArch(osArch),
DownloadURL: asset.DownloadURL,
}
}
return tools, nil
}