Files
wild-central-api/internal/assets/assets.go

449 lines
12 KiB
Go

package assets
import (
"crypto/sha256"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/wild-cloud/wild-central/daemon/internal/storage"
)
// Manager handles centralized Talos asset management
type Manager struct {
dataDir string
}
// NewManager creates a new asset manager
func NewManager(dataDir string) *Manager {
return &Manager{
dataDir: dataDir,
}
}
// Asset represents a Talos boot asset
type Asset struct {
Type string `json:"type"` // kernel, initramfs, iso
Path string `json:"path"` // Full path to asset file
Size int64 `json:"size"` // File size in bytes
SHA256 string `json:"sha256"` // SHA256 hash
Downloaded bool `json:"downloaded"` // Whether asset exists
}
// PXEAsset represents a schematic@version combination and its assets
type PXEAsset struct {
SchematicID string `json:"schematic_id"`
Version string `json:"version"`
Path string `json:"path"`
Assets []Asset `json:"assets"`
}
// AssetStatus represents download status for a schematic
type AssetStatus struct {
SchematicID string `json:"schematic_id"`
Version string `json:"version"`
Assets map[string]Asset `json:"assets"`
Complete bool `json:"complete"`
}
// GetAssetDir returns the asset directory for a schematic@version composite key
func (m *Manager) GetAssetDir(schematicID, version string) string {
composite := fmt.Sprintf("%s@%s", schematicID, version)
return filepath.Join(m.dataDir, "assets", composite)
}
// GetAssetsRootDir returns the root assets directory
func (m *Manager) GetAssetsRootDir() string {
return filepath.Join(m.dataDir, "assets")
}
// ListAssets returns all available schematic@version combinations
func (m *Manager) ListAssets() ([]PXEAsset, error) {
assetsDir := m.GetAssetsRootDir()
// Ensure assets directory exists
if err := storage.EnsureDir(assetsDir, 0755); err != nil {
return nil, fmt.Errorf("ensuring assets directory: %w", err)
}
entries, err := os.ReadDir(assetsDir)
if err != nil {
return nil, fmt.Errorf("reading assets directory: %w", err)
}
var assets []PXEAsset
for _, entry := range entries {
if entry.IsDir() {
// Parse directory name as schematicID@version
parts := strings.SplitN(entry.Name(), "@", 2)
if len(parts) != 2 {
// Skip invalid directory names (old format or other)
continue
}
schematicID := parts[0]
version := parts[1]
asset, err := m.GetAsset(schematicID, version)
if err != nil {
// Skip invalid assets
continue
}
assets = append(assets, *asset)
}
}
return assets, nil
}
// GetAsset returns details for a specific schematic@version combination
func (m *Manager) GetAsset(schematicID, version string) (*PXEAsset, error) {
if schematicID == "" {
return nil, fmt.Errorf("schematic ID cannot be empty")
}
if version == "" {
return nil, fmt.Errorf("version cannot be empty")
}
assetDir := m.GetAssetDir(schematicID, version)
// Check if asset directory exists
if !storage.FileExists(assetDir) {
return nil, fmt.Errorf("asset %s@%s not found", schematicID, version)
}
// List assets for this schematic@version
assets, err := m.listAssetFiles(schematicID, version)
if err != nil {
return nil, fmt.Errorf("listing assets: %w", err)
}
return &PXEAsset{
SchematicID: schematicID,
Version: version,
Path: assetDir,
Assets: assets,
}, nil
}
// AssetExists checks if a schematic@version exists
func (m *Manager) AssetExists(schematicID, version string) bool {
return storage.FileExists(m.GetAssetDir(schematicID, version))
}
// listAssetFiles lists all asset files for a schematic@version
func (m *Manager) listAssetFiles(schematicID, version string) ([]Asset, error) {
assetDir := m.GetAssetDir(schematicID, version)
var assets []Asset
// Check for PXE assets (kernel and initramfs for both platforms)
pxeDir := filepath.Join(assetDir, "pxe")
pxePatterns := []string{
"kernel-amd64",
"kernel-arm64",
"initramfs-amd64.xz",
"initramfs-arm64.xz",
}
for _, pattern := range pxePatterns {
assetPath := filepath.Join(pxeDir, pattern)
info, err := os.Stat(assetPath)
var assetType string
if strings.HasPrefix(pattern, "kernel-") {
assetType = "kernel"
} else {
assetType = "initramfs"
}
asset := Asset{
Type: assetType,
Path: assetPath,
Downloaded: err == nil,
}
if err == nil && info != nil {
asset.Size = info.Size()
// Calculate SHA256 if file exists
if hash, err := calculateSHA256(assetPath); err == nil {
asset.SHA256 = hash
}
}
assets = append(assets, asset)
}
// Check for ISO assets (glob pattern to find all ISOs)
isoDir := filepath.Join(assetDir, "iso")
isoMatches, err := filepath.Glob(filepath.Join(isoDir, "talos-*.iso"))
if err == nil {
for _, isoPath := range isoMatches {
info, err := os.Stat(isoPath)
asset := Asset{
Type: "iso",
Path: isoPath,
Downloaded: err == nil,
}
if err == nil && info != nil {
asset.Size = info.Size()
// Calculate SHA256 if file exists
if hash, err := calculateSHA256(isoPath); err == nil {
asset.SHA256 = hash
}
}
assets = append(assets, asset)
}
}
return assets, nil
}
// DownloadAssets downloads specified assets for a schematic
func (m *Manager) DownloadAssets(schematicID, version, platform string, assetTypes []string) error {
if schematicID == "" {
return fmt.Errorf("schematic ID cannot be empty")
}
if version == "" {
return fmt.Errorf("version cannot be empty")
}
if platform == "" {
platform = "amd64" // Default to amd64
}
// Validate platform
if platform != "amd64" && platform != "arm64" {
return fmt.Errorf("invalid platform: %s (must be amd64 or arm64)", platform)
}
if len(assetTypes) == 0 {
// Default to all asset types
assetTypes = []string{"kernel", "initramfs", "iso"}
}
assetDir := m.GetAssetDir(schematicID, version)
// Ensure asset directory exists
if err := storage.EnsureDir(assetDir, 0755); err != nil {
return fmt.Errorf("creating asset directory: %w", err)
}
// Download each requested asset
for _, assetType := range assetTypes {
if err := m.downloadAsset(schematicID, assetType, version, platform); err != nil {
return fmt.Errorf("downloading %s: %w", assetType, err)
}
}
return nil
}
// downloadAsset downloads a single asset
func (m *Manager) downloadAsset(schematicID, assetType, version, platform string) error {
assetDir := m.GetAssetDir(schematicID, version)
// Determine subdirectory, filename, and URL based on asset type and platform
var subdir, filename, urlPath string
switch assetType {
case "kernel":
subdir = "pxe"
filename = fmt.Sprintf("kernel-%s", platform)
urlPath = fmt.Sprintf("kernel-%s", platform)
case "initramfs":
subdir = "pxe"
filename = fmt.Sprintf("initramfs-%s.xz", platform)
urlPath = fmt.Sprintf("initramfs-%s.xz", platform)
case "iso":
subdir = "iso"
// Include version in filename for clarity
filename = fmt.Sprintf("talos-%s-metal-%s.iso", version, platform)
urlPath = fmt.Sprintf("metal-%s.iso", platform)
default:
return fmt.Errorf("unknown asset type: %s", assetType)
}
// Create subdirectory structure
assetTypeDir := filepath.Join(assetDir, subdir)
if err := storage.EnsureDir(assetTypeDir, 0755); err != nil {
return fmt.Errorf("creating %s directory: %w", subdir, err)
}
assetPath := filepath.Join(assetTypeDir, filename)
// Skip if asset already exists (idempotency)
if storage.FileExists(assetPath) {
return nil
}
// Construct download URL from Image Factory
url := fmt.Sprintf("https://factory.talos.dev/image/%s/%s/%s", schematicID, version, urlPath)
// Download file
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("downloading from %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download failed with status %d from %s", resp.StatusCode, url)
}
// Create temporary file
tmpFile := assetPath + ".tmp"
out, err := os.Create(tmpFile)
if err != nil {
return fmt.Errorf("creating temporary file: %w", err)
}
defer out.Close()
// Copy data
_, err = io.Copy(out, resp.Body)
if err != nil {
os.Remove(tmpFile)
return fmt.Errorf("writing file: %w", err)
}
// Close file before rename
out.Close()
// Move to final location
if err := os.Rename(tmpFile, assetPath); err != nil {
os.Remove(tmpFile)
return fmt.Errorf("moving file to final location: %w", err)
}
return nil
}
// GetAssetStatus returns the download status for a schematic@version
func (m *Manager) GetAssetStatus(schematicID, version string) (*AssetStatus, error) {
if schematicID == "" {
return nil, fmt.Errorf("schematic ID cannot be empty")
}
if version == "" {
return nil, fmt.Errorf("version cannot be empty")
}
assetDir := m.GetAssetDir(schematicID, version)
// Check if asset directory exists
if !storage.FileExists(assetDir) {
return nil, fmt.Errorf("asset %s@%s not found", schematicID, version)
}
// List assets
assets, err := m.listAssetFiles(schematicID, version)
if err != nil {
return nil, fmt.Errorf("listing assets: %w", err)
}
// Build asset map and check completion
assetMap := make(map[string]Asset)
complete := true
for _, asset := range assets {
assetMap[asset.Type] = asset
if !asset.Downloaded {
complete = false
}
}
return &AssetStatus{
SchematicID: schematicID,
Version: version,
Assets: assetMap,
Complete: complete,
}, nil
}
// GetAssetPath returns the path to a specific asset file
func (m *Manager) GetAssetPath(schematicID, version, assetType string) (string, error) {
if schematicID == "" {
return "", fmt.Errorf("schematic ID cannot be empty")
}
if version == "" {
return "", fmt.Errorf("version cannot be empty")
}
assetDir := m.GetAssetDir(schematicID, version)
var subdir, pattern string
switch assetType {
case "kernel":
subdir = "pxe"
pattern = "kernel-amd64"
case "initramfs":
subdir = "pxe"
pattern = "initramfs-amd64.xz"
case "iso":
subdir = "iso"
pattern = "talos-*.iso" // Glob pattern for version and platform-specific filename
default:
return "", fmt.Errorf("unknown asset type: %s", assetType)
}
assetTypeDir := filepath.Join(assetDir, subdir)
// Find matching file (supports glob pattern for ISO)
var assetPath string
if strings.Contains(pattern, "*") {
matches, err := filepath.Glob(filepath.Join(assetTypeDir, pattern))
if err != nil {
return "", fmt.Errorf("searching for asset: %w", err)
}
if len(matches) == 0 {
return "", fmt.Errorf("asset %s not found for schematic %s", assetType, schematicID)
}
assetPath = matches[0] // Use first match
} else {
assetPath = filepath.Join(assetTypeDir, pattern)
}
if !storage.FileExists(assetPath) {
return "", fmt.Errorf("asset %s not found for schematic %s", assetType, schematicID)
}
return assetPath, nil
}
// DeleteAsset removes a schematic@version and all its assets
func (m *Manager) DeleteAsset(schematicID, version string) error {
if schematicID == "" {
return fmt.Errorf("schematic ID cannot be empty")
}
if version == "" {
return fmt.Errorf("version cannot be empty")
}
assetDir := m.GetAssetDir(schematicID, version)
if !storage.FileExists(assetDir) {
return nil // Already deleted, idempotent
}
return os.RemoveAll(assetDir)
}
// calculateSHA256 computes the SHA256 hash of a file
func calculateSHA256(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}