Better support for Talos ISO downloads.
This commit is contained in:
448
internal/assets/assets.go
Normal file
448
internal/assets/assets.go
Normal file
@@ -0,0 +1,448 @@
|
||||
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
|
||||
}
|
||||
|
||||
// Schematic represents a Talos schematic and its assets
|
||||
type Schematic 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
|
||||
func (m *Manager) GetAssetDir(schematicID string) string {
|
||||
return filepath.Join(m.dataDir, "assets", schematicID)
|
||||
}
|
||||
|
||||
// GetAssetsRootDir returns the root assets directory
|
||||
func (m *Manager) GetAssetsRootDir() string {
|
||||
return filepath.Join(m.dataDir, "assets")
|
||||
}
|
||||
|
||||
// ListSchematics returns all available schematics
|
||||
func (m *Manager) ListSchematics() ([]Schematic, 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 schematics []Schematic
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
schematicID := entry.Name()
|
||||
schematic, err := m.GetSchematic(schematicID)
|
||||
if err != nil {
|
||||
// Skip invalid schematics
|
||||
continue
|
||||
}
|
||||
schematics = append(schematics, *schematic)
|
||||
}
|
||||
}
|
||||
|
||||
return schematics, nil
|
||||
}
|
||||
|
||||
// GetSchematic returns details for a specific schematic
|
||||
func (m *Manager) GetSchematic(schematicID string) (*Schematic, error) {
|
||||
if schematicID == "" {
|
||||
return nil, fmt.Errorf("schematic ID cannot be empty")
|
||||
}
|
||||
|
||||
assetDir := m.GetAssetDir(schematicID)
|
||||
|
||||
// Check if schematic directory exists
|
||||
if !storage.FileExists(assetDir) {
|
||||
return nil, fmt.Errorf("schematic %s not found", schematicID)
|
||||
}
|
||||
|
||||
// List assets for this schematic
|
||||
assets, err := m.listSchematicAssets(schematicID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing schematic assets: %w", err)
|
||||
}
|
||||
|
||||
// Try to determine version from version file
|
||||
version := ""
|
||||
versionPath := filepath.Join(assetDir, "version.txt")
|
||||
if storage.FileExists(versionPath) {
|
||||
data, err := os.ReadFile(versionPath)
|
||||
if err == nil {
|
||||
version = strings.TrimSpace(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return &Schematic{
|
||||
SchematicID: schematicID,
|
||||
Version: version,
|
||||
Path: assetDir,
|
||||
Assets: assets,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// listSchematicAssets lists all assets for a schematic
|
||||
func (m *Manager) listSchematicAssets(schematicID string) ([]Asset, error) {
|
||||
assetDir := m.GetAssetDir(schematicID)
|
||||
|
||||
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)
|
||||
|
||||
// Ensure asset directory exists
|
||||
if err := storage.EnsureDir(assetDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating asset directory: %w", err)
|
||||
}
|
||||
|
||||
// Save version info
|
||||
versionPath := filepath.Join(assetDir, "version.txt")
|
||||
if err := os.WriteFile(versionPath, []byte(version), 0644); err != nil {
|
||||
return fmt.Errorf("saving version info: %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)
|
||||
|
||||
// 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"
|
||||
// Preserve version and platform 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
|
||||
func (m *Manager) GetAssetStatus(schematicID string) (*AssetStatus, error) {
|
||||
if schematicID == "" {
|
||||
return nil, fmt.Errorf("schematic ID cannot be empty")
|
||||
}
|
||||
|
||||
assetDir := m.GetAssetDir(schematicID)
|
||||
|
||||
// Check if schematic directory exists
|
||||
if !storage.FileExists(assetDir) {
|
||||
return nil, fmt.Errorf("schematic %s not found", schematicID)
|
||||
}
|
||||
|
||||
// Get version
|
||||
version := ""
|
||||
versionPath := filepath.Join(assetDir, "version.txt")
|
||||
if storage.FileExists(versionPath) {
|
||||
data, err := os.ReadFile(versionPath)
|
||||
if err == nil {
|
||||
version = strings.TrimSpace(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// List assets
|
||||
assets, err := m.listSchematicAssets(schematicID)
|
||||
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, assetType string) (string, error) {
|
||||
if schematicID == "" {
|
||||
return "", fmt.Errorf("schematic ID cannot be empty")
|
||||
}
|
||||
|
||||
assetDir := m.GetAssetDir(schematicID)
|
||||
|
||||
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-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
|
||||
}
|
||||
|
||||
// DeleteSchematic removes a schematic and all its assets
|
||||
func (m *Manager) DeleteSchematic(schematicID string) error {
|
||||
if schematicID == "" {
|
||||
return fmt.Errorf("schematic ID cannot be empty")
|
||||
}
|
||||
|
||||
assetDir := m.GetAssetDir(schematicID)
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user