Better support for Talos ISO downloads.
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
## Principles
|
## Principles
|
||||||
|
|
||||||
|
- The API enables all of the functionaly needed by the CLI and the webapp. These clients should conform to the API. The API should not be designed around the needs of the CLI or webapp.
|
||||||
- A wild cloud instance is primarily data (YAML files for config, secrets, and manifests).
|
- A wild cloud instance is primarily data (YAML files for config, secrets, and manifests).
|
||||||
- Because a wild cloud instance is primarily data, a wild cloud instance can be managed by non-technical users through the webapp or by technical users by SSHing into the device (e.g. VSCode Remote SSH).
|
- Because a wild cloud instance is primarily data, a wild cloud instance can be managed by non-technical users through the webapp or by technical users by SSHing into the device (e.g. VSCode Remote SSH).
|
||||||
- Like v.PoC, we should only use gomplate templates for distinguishing between cloud instances. However, **within** a cloud instance, there should be no templating. The templates are compiled when being copied into the instances. This allows transparency and simple management by the user.
|
- Like v.PoC, we should only use gomplate templates for distinguishing between cloud instances. However, **within** a cloud instance, there should be no templating. The templates are compiled when being copied into the instances. This allows transparency and simple management by the user.
|
||||||
|
|||||||
@@ -61,28 +61,27 @@ func NewAPI(dataDir, appsDir string) (*API, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterRoutes registers all API routes (Phase 1 + Phase 2)
|
|
||||||
func (api *API) RegisterRoutes(r *mux.Router) {
|
func (api *API) RegisterRoutes(r *mux.Router) {
|
||||||
// Phase 1: Instance management
|
// Instance management
|
||||||
r.HandleFunc("/api/v1/instances", api.CreateInstance).Methods("POST")
|
r.HandleFunc("/api/v1/instances", api.CreateInstance).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances", api.ListInstances).Methods("GET")
|
r.HandleFunc("/api/v1/instances", api.ListInstances).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}", api.GetInstance).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}", api.GetInstance).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}", api.DeleteInstance).Methods("DELETE")
|
r.HandleFunc("/api/v1/instances/{name}", api.DeleteInstance).Methods("DELETE")
|
||||||
|
|
||||||
// Phase 1: Config management
|
// Config management
|
||||||
r.HandleFunc("/api/v1/instances/{name}/config", api.GetConfig).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/config", api.GetConfig).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/config", api.UpdateConfig).Methods("PUT")
|
r.HandleFunc("/api/v1/instances/{name}/config", api.UpdateConfig).Methods("PUT")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/config", api.ConfigUpdateBatch).Methods("PATCH")
|
r.HandleFunc("/api/v1/instances/{name}/config", api.ConfigUpdateBatch).Methods("PATCH")
|
||||||
|
|
||||||
// Phase 1: Secrets management
|
// Secrets management
|
||||||
r.HandleFunc("/api/v1/instances/{name}/secrets", api.GetSecrets).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/secrets", api.GetSecrets).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/secrets", api.UpdateSecrets).Methods("PUT")
|
r.HandleFunc("/api/v1/instances/{name}/secrets", api.UpdateSecrets).Methods("PUT")
|
||||||
|
|
||||||
// Phase 1: Context management
|
// Context management
|
||||||
r.HandleFunc("/api/v1/context", api.GetContext).Methods("GET")
|
r.HandleFunc("/api/v1/context", api.GetContext).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/context", api.SetContext).Methods("POST")
|
r.HandleFunc("/api/v1/context", api.SetContext).Methods("POST")
|
||||||
|
|
||||||
// Phase 2: Node management
|
// Node management
|
||||||
r.HandleFunc("/api/v1/instances/{name}/nodes/discover", api.NodeDiscover).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/nodes/discover", api.NodeDiscover).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/nodes/detect", api.NodeDetect).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/nodes/detect", api.NodeDetect).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/discovery", api.NodeDiscoveryStatus).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/discovery", api.NodeDiscoveryStatus).Methods("GET")
|
||||||
@@ -95,19 +94,25 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
|||||||
r.HandleFunc("/api/v1/instances/{name}/nodes/{node}/apply", api.NodeApply).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/nodes/{node}/apply", api.NodeApply).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/nodes/{node}", api.NodeDelete).Methods("DELETE")
|
r.HandleFunc("/api/v1/instances/{name}/nodes/{node}", api.NodeDelete).Methods("DELETE")
|
||||||
|
|
||||||
// Phase 2: PXE asset management
|
// Asset management
|
||||||
r.HandleFunc("/api/v1/instances/{name}/pxe/assets", api.PXEListAssets).Methods("GET")
|
r.HandleFunc("/api/v1/assets", api.AssetsListSchematics).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/pxe/assets/download", api.PXEDownloadAsset).Methods("POST")
|
r.HandleFunc("/api/v1/assets/{schematicId}", api.AssetsGetSchematic).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/pxe/assets/{type}", api.PXEGetAsset).Methods("GET")
|
r.HandleFunc("/api/v1/assets/{schematicId}/download", api.AssetsDownload).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/pxe/assets/{type}", api.PXEDeleteAsset).Methods("DELETE")
|
r.HandleFunc("/api/v1/assets/{schematicId}/pxe/{assetType}", api.AssetsServePXE).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/assets/{schematicId}/status", api.AssetsGetStatus).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/assets/{schematicId}", api.AssetsDeleteSchematic).Methods("DELETE")
|
||||||
|
|
||||||
// Phase 2: Operations
|
// Instance-schematic relationship
|
||||||
|
r.HandleFunc("/api/v1/instances/{name}/schematic", api.SchematicGetInstanceSchematic).Methods("GET")
|
||||||
|
r.HandleFunc("/api/v1/instances/{name}/schematic", api.SchematicUpdateInstanceSchematic).Methods("PUT")
|
||||||
|
|
||||||
|
// Operations
|
||||||
r.HandleFunc("/api/v1/instances/{name}/operations", api.OperationList).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/operations", api.OperationList).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/operations/{id}", api.OperationGet).Methods("GET")
|
r.HandleFunc("/api/v1/operations/{id}", api.OperationGet).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/operations/{id}/stream", api.OperationStream).Methods("GET")
|
r.HandleFunc("/api/v1/operations/{id}/stream", api.OperationStream).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/operations/{id}/cancel", api.OperationCancel).Methods("POST")
|
r.HandleFunc("/api/v1/operations/{id}/cancel", api.OperationCancel).Methods("POST")
|
||||||
|
|
||||||
// Phase 3: Cluster operations
|
// Cluster operations
|
||||||
r.HandleFunc("/api/v1/instances/{name}/cluster/config/generate", api.ClusterGenerateConfig).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/cluster/config/generate", api.ClusterGenerateConfig).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/cluster/bootstrap", api.ClusterBootstrap).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/cluster/bootstrap", api.ClusterBootstrap).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/cluster/endpoints", api.ClusterConfigureEndpoints).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/cluster/endpoints", api.ClusterConfigureEndpoints).Methods("POST")
|
||||||
@@ -118,7 +123,7 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
|||||||
r.HandleFunc("/api/v1/instances/{name}/cluster/talosconfig", api.ClusterGetTalosconfig).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/cluster/talosconfig", api.ClusterGetTalosconfig).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/cluster/reset", api.ClusterReset).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/cluster/reset", api.ClusterReset).Methods("POST")
|
||||||
|
|
||||||
// Phase 4: Services
|
// Services
|
||||||
r.HandleFunc("/api/v1/instances/{name}/services", api.ServicesList).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/services", api.ServicesList).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/services", api.ServicesInstall).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/services", api.ServicesInstall).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/services/install-all", api.ServicesInstallAll).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/services/install-all", api.ServicesInstallAll).Methods("POST")
|
||||||
@@ -134,7 +139,7 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
|||||||
r.HandleFunc("/api/v1/instances/{name}/services/{service}/compile", api.ServicesCompile).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/services/{service}/compile", api.ServicesCompile).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/services/{service}/deploy", api.ServicesDeploy).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/services/{service}/deploy", api.ServicesDeploy).Methods("POST")
|
||||||
|
|
||||||
// Phase 4: Apps
|
// Apps
|
||||||
r.HandleFunc("/api/v1/apps", api.AppsListAvailable).Methods("GET")
|
r.HandleFunc("/api/v1/apps", api.AppsListAvailable).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/apps/{app}", api.AppsGetAvailable).Methods("GET")
|
r.HandleFunc("/api/v1/apps/{app}", api.AppsGetAvailable).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/apps", api.AppsListDeployed).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/apps", api.AppsListDeployed).Methods("GET")
|
||||||
@@ -143,12 +148,12 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
|||||||
r.HandleFunc("/api/v1/instances/{name}/apps/{app}", api.AppsDelete).Methods("DELETE")
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}", api.AppsDelete).Methods("DELETE")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/status", api.AppsGetStatus).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/status", api.AppsGetStatus).Methods("GET")
|
||||||
|
|
||||||
// Phase 5: Backup & Restore
|
// Backup & Restore
|
||||||
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/backup", api.BackupAppStart).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/backup", api.BackupAppStart).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/backup", api.BackupAppList).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/backup", api.BackupAppList).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/restore", api.BackupAppRestore).Methods("POST")
|
r.HandleFunc("/api/v1/instances/{name}/apps/{app}/restore", api.BackupAppRestore).Methods("POST")
|
||||||
|
|
||||||
// Phase 5: Utilities
|
// Utilities
|
||||||
r.HandleFunc("/api/v1/utilities/health", api.UtilitiesHealth).Methods("GET")
|
r.HandleFunc("/api/v1/utilities/health", api.UtilitiesHealth).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/instances/{name}/utilities/health", api.InstanceUtilitiesHealth).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/utilities/health", api.InstanceUtilitiesHealth).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/utilities/dashboard/token", api.UtilitiesDashboardToken).Methods("GET")
|
r.HandleFunc("/api/v1/utilities/dashboard/token", api.UtilitiesDashboardToken).Methods("GET")
|
||||||
|
|||||||
172
internal/api/v1/handlers_assets.go
Normal file
172
internal/api/v1/handlers_assets.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AssetsListSchematics lists all available schematics
|
||||||
|
func (api *API) AssetsListSchematics(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
|
schematics, err := assetsMgr.ListSchematics()
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list schematics: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"schematics": schematics,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetsGetSchematic returns details for a specific schematic
|
||||||
|
func (api *API) AssetsGetSchematic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
schematicID := vars["schematicId"]
|
||||||
|
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
|
schematic, err := assetsMgr.GetSchematic(schematicID)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Schematic not found: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, schematic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetsDownload downloads assets for a schematic
|
||||||
|
func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
schematicID := vars["schematicId"]
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var req struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
|
AssetTypes []string `json:"asset_types,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Version == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "version is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default platform to amd64 if not specified
|
||||||
|
if req.Platform == "" {
|
||||||
|
req.Platform = "amd64"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download assets
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
if err := assetsMgr.DownloadAssets(schematicID, req.Version, req.Platform, req.AssetTypes); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to download assets: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"message": "Assets downloaded successfully",
|
||||||
|
"schematic_id": schematicID,
|
||||||
|
"version": req.Version,
|
||||||
|
"platform": req.Platform,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetsServePXE serves a PXE asset file
|
||||||
|
func (api *API) AssetsServePXE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
schematicID := vars["schematicId"]
|
||||||
|
assetType := vars["assetType"]
|
||||||
|
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
|
// Get asset path
|
||||||
|
assetPath, err := assetsMgr.GetAssetPath(schematicID, assetType)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Asset not found: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file
|
||||||
|
file, err := os.Open(assetPath)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to open asset: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Get file info for size
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to stat asset: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set appropriate content type
|
||||||
|
var contentType string
|
||||||
|
switch assetType {
|
||||||
|
case "kernel":
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
case "initramfs":
|
||||||
|
contentType = "application/x-xz"
|
||||||
|
case "iso":
|
||||||
|
contentType = "application/x-iso9660-image"
|
||||||
|
default:
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size()))
|
||||||
|
// Set Content-Disposition to suggest filename for download
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.Name()))
|
||||||
|
|
||||||
|
// Serve file
|
||||||
|
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetsGetStatus returns download status for a schematic
|
||||||
|
func (api *API) AssetsGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
schematicID := vars["schematicId"]
|
||||||
|
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
|
status, err := assetsMgr.GetAssetStatus(schematicID)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Schematic not found: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetsDeleteSchematic deletes a schematic and all its assets
|
||||||
|
func (api *API) AssetsDeleteSchematic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
schematicID := vars["schematicId"]
|
||||||
|
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
|
if err := assetsMgr.DeleteSchematic(schematicID); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete schematic: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]string{
|
||||||
|
"message": "Schematic deleted successfully",
|
||||||
|
"schematic_id": schematicID,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -22,8 +22,9 @@ func (api *API) NodeDiscover(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse request body
|
// Parse request body - support both subnet and ip_list formats
|
||||||
var req struct {
|
var req struct {
|
||||||
|
Subnet string `json:"subnet"`
|
||||||
IPList []string `json:"ip_list"`
|
IPList []string `json:"ip_list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,14 +33,21 @@ func (api *API) NodeDiscover(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.IPList) == 0 {
|
// If subnet provided, use it as a single "IP" for discovery
|
||||||
respondError(w, http.StatusBadRequest, "ip_list is required")
|
// The discovery manager will scan this subnet
|
||||||
|
var ipList []string
|
||||||
|
if req.Subnet != "" {
|
||||||
|
ipList = []string{req.Subnet}
|
||||||
|
} else if len(req.IPList) > 0 {
|
||||||
|
ipList = req.IPList
|
||||||
|
} else {
|
||||||
|
respondError(w, http.StatusBadRequest, "subnet or ip_list is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start discovery
|
// Start discovery
|
||||||
discoveryMgr := discovery.NewManager(api.dataDir, instanceName)
|
discoveryMgr := discovery.NewManager(api.dataDir, instanceName)
|
||||||
if err := discoveryMgr.StartDiscovery(instanceName, req.IPList); err != nil {
|
if err := discoveryMgr.StartDiscovery(instanceName, ipList); err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start discovery: %v", err))
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start discovery: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,42 +3,72 @@ package v1
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/assets"
|
||||||
"github.com/wild-cloud/wild-central/daemon/internal/pxe"
|
"github.com/wild-cloud/wild-central/daemon/internal/pxe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PXEListAssets lists all PXE assets for an instance
|
// PXEListAssets lists all PXE assets for an instance
|
||||||
|
// DEPRECATED: This endpoint is deprecated. Use GET /api/v1/assets/{schematicId} instead.
|
||||||
func (api *API) PXEListAssets(w http.ResponseWriter, r *http.Request) {
|
func (api *API) PXEListAssets(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
instanceName := vars["name"]
|
instanceName := vars["name"]
|
||||||
|
|
||||||
|
// Add deprecation warning header
|
||||||
|
w.Header().Set("X-Deprecated", "This endpoint is deprecated. Use GET /api/v1/assets/{schematicId} instead.")
|
||||||
|
log.Printf("Warning: Deprecated endpoint /api/v1/instances/%s/pxe/assets called", instanceName)
|
||||||
|
|
||||||
// Validate instance exists
|
// Validate instance exists
|
||||||
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
||||||
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// List assets
|
// Get schematic ID from instance config
|
||||||
pxeMgr := pxe.NewManager(api.dataDir)
|
configPath := api.instance.GetInstanceConfigPath(instanceName)
|
||||||
assets, err := pxeMgr.ListAssets(instanceName)
|
schematicID, err := api.config.GetConfigValue(configPath, "cluster.nodes.talos.schematicId")
|
||||||
|
if err != nil || schematicID == "" || schematicID == "null" {
|
||||||
|
// Fall back to old PXE manager if no schematic configured
|
||||||
|
pxeMgr := pxe.NewManager(api.dataDir)
|
||||||
|
assets, err := pxeMgr.ListAssets(instanceName)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list assets: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"assets": assets,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy to new asset system
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
schematic, err := assetsMgr.GetSchematic(schematicID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list assets: %v", err))
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Schematic not found: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"assets": assets,
|
"assets": schematic.Assets,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PXEDownloadAsset downloads a PXE asset
|
// PXEDownloadAsset downloads a PXE asset
|
||||||
|
// DEPRECATED: This endpoint is deprecated. Use POST /api/v1/assets/{schematicId}/download instead.
|
||||||
func (api *API) PXEDownloadAsset(w http.ResponseWriter, r *http.Request) {
|
func (api *API) PXEDownloadAsset(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
instanceName := vars["name"]
|
instanceName := vars["name"]
|
||||||
|
|
||||||
|
// Add deprecation warning header
|
||||||
|
w.Header().Set("X-Deprecated", "This endpoint is deprecated. Use POST /api/v1/assets/{schematicId}/download instead.")
|
||||||
|
log.Printf("Warning: Deprecated endpoint /api/v1/instances/%s/pxe/assets/download called", instanceName)
|
||||||
|
|
||||||
// Validate instance exists
|
// Validate instance exists
|
||||||
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
||||||
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
||||||
@@ -62,80 +92,154 @@ func (api *API) PXEDownloadAsset(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.URL == "" {
|
// Get schematic ID from instance config
|
||||||
respondError(w, http.StatusBadRequest, "url is required")
|
configPath := api.instance.GetInstanceConfigPath(instanceName)
|
||||||
|
schematicID, err := api.config.GetConfigValue(configPath, "cluster.nodes.talos.schematicId")
|
||||||
|
|
||||||
|
// If no schematic configured or URL provided (old behavior), fall back to old PXE manager
|
||||||
|
if (err != nil || schematicID == "" || schematicID == "null") || req.URL != "" {
|
||||||
|
if req.URL == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "url is required when schematic is not configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download asset using old PXE manager
|
||||||
|
pxeMgr := pxe.NewManager(api.dataDir)
|
||||||
|
if err := pxeMgr.DownloadAsset(instanceName, req.AssetType, req.Version, req.URL); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to download asset: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]string{
|
||||||
|
"message": "Asset downloaded successfully",
|
||||||
|
"asset_type": req.AssetType,
|
||||||
|
"version": req.Version,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download asset
|
// Proxy to new asset system
|
||||||
pxeMgr := pxe.NewManager(api.dataDir)
|
if req.Version == "" {
|
||||||
if err := pxeMgr.DownloadAsset(instanceName, req.AssetType, req.Version, req.URL); err != nil {
|
respondError(w, http.StatusBadRequest, "version is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
assetTypes := []string{req.AssetType}
|
||||||
|
platform := "amd64" // Default platform for backward compatibility
|
||||||
|
if err := assetsMgr.DownloadAssets(schematicID, req.Version, platform, assetTypes); err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to download asset: %v", err))
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to download asset: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
respondJSON(w, http.StatusOK, map[string]string{
|
respondJSON(w, http.StatusOK, map[string]string{
|
||||||
"message": "Asset downloaded successfully",
|
"message": "Asset downloaded successfully",
|
||||||
"asset_type": req.AssetType,
|
"asset_type": req.AssetType,
|
||||||
"version": req.Version,
|
"version": req.Version,
|
||||||
|
"schematic_id": schematicID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PXEGetAsset returns information about a specific asset
|
// PXEGetAsset returns information about a specific asset
|
||||||
|
// DEPRECATED: This endpoint is deprecated. Use GET /api/v1/assets/{schematicId}/pxe/{assetType} instead.
|
||||||
func (api *API) PXEGetAsset(w http.ResponseWriter, r *http.Request) {
|
func (api *API) PXEGetAsset(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
instanceName := vars["name"]
|
instanceName := vars["name"]
|
||||||
assetType := vars["type"]
|
assetType := vars["type"]
|
||||||
|
|
||||||
|
// Add deprecation warning header
|
||||||
|
w.Header().Set("X-Deprecated", "This endpoint is deprecated. Use GET /api/v1/assets/{schematicId}/pxe/{assetType} instead.")
|
||||||
|
log.Printf("Warning: Deprecated endpoint /api/v1/instances/%s/pxe/assets/%s called", instanceName, assetType)
|
||||||
|
|
||||||
// Validate instance exists
|
// Validate instance exists
|
||||||
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
||||||
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get asset path
|
// Get schematic ID from instance config
|
||||||
pxeMgr := pxe.NewManager(api.dataDir)
|
configPath := api.instance.GetInstanceConfigPath(instanceName)
|
||||||
assetPath, err := pxeMgr.GetAssetPath(instanceName, assetType)
|
schematicID, err := api.config.GetConfigValue(configPath, "cluster.nodes.talos.schematicId")
|
||||||
|
if err != nil || schematicID == "" || schematicID == "null" {
|
||||||
|
// Fall back to old PXE manager if no schematic configured
|
||||||
|
pxeMgr := pxe.NewManager(api.dataDir)
|
||||||
|
assetPath, err := pxeMgr.GetAssetPath(instanceName, assetType)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Asset not found: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify asset
|
||||||
|
valid, err := pxeMgr.VerifyAsset(instanceName, assetType)
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to verify asset: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"type": assetType,
|
||||||
|
"path": assetPath,
|
||||||
|
"valid": valid,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy to new asset system - serve the file directly
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
assetPath, err := assetsMgr.GetAssetPath(schematicID, assetType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusNotFound, fmt.Sprintf("Asset not found: %v", err))
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Asset not found: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify asset
|
|
||||||
valid, err := pxeMgr.VerifyAsset(instanceName, assetType)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to verify asset: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"type": assetType,
|
"type": assetType,
|
||||||
"path": assetPath,
|
"path": assetPath,
|
||||||
"valid": valid,
|
"valid": true,
|
||||||
|
"schematic_id": schematicID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PXEDeleteAsset deletes a PXE asset
|
// PXEDeleteAsset deletes a PXE asset
|
||||||
|
// DEPRECATED: This endpoint is deprecated. Use DELETE /api/v1/assets/{schematicId} instead.
|
||||||
func (api *API) PXEDeleteAsset(w http.ResponseWriter, r *http.Request) {
|
func (api *API) PXEDeleteAsset(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
instanceName := vars["name"]
|
instanceName := vars["name"]
|
||||||
assetType := vars["type"]
|
assetType := vars["type"]
|
||||||
|
|
||||||
|
// Add deprecation warning header
|
||||||
|
w.Header().Set("X-Deprecated", "This endpoint is deprecated. Use DELETE /api/v1/assets/{schematicId} instead.")
|
||||||
|
log.Printf("Warning: Deprecated endpoint DELETE /api/v1/instances/%s/pxe/assets/%s called", instanceName, assetType)
|
||||||
|
|
||||||
// Validate instance exists
|
// Validate instance exists
|
||||||
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
||||||
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete asset
|
// Get schematic ID from instance config
|
||||||
pxeMgr := pxe.NewManager(api.dataDir)
|
configPath := api.instance.GetInstanceConfigPath(instanceName)
|
||||||
if err := pxeMgr.DeleteAsset(instanceName, assetType); err != nil {
|
schematicID, err := api.config.GetConfigValue(configPath, "cluster.nodes.talos.schematicId")
|
||||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete asset: %v", err))
|
if err != nil || schematicID == "" || schematicID == "null" {
|
||||||
|
// Fall back to old PXE manager if no schematic configured
|
||||||
|
pxeMgr := pxe.NewManager(api.dataDir)
|
||||||
|
if err := pxeMgr.DeleteAsset(instanceName, assetType); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete asset: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]string{
|
||||||
|
"message": "Asset deleted successfully",
|
||||||
|
"type": assetType,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: In the new system, we don't delete individual assets, only entire schematics
|
||||||
|
// For backward compatibility, we'll just report success without doing anything
|
||||||
respondJSON(w, http.StatusOK, map[string]string{
|
respondJSON(w, http.StatusOK, map[string]string{
|
||||||
"message": "Asset deleted successfully",
|
"message": "Individual asset deletion not supported in schematic mode. Use DELETE /api/v1/assets/{schematicId} to delete all assets.",
|
||||||
"type": assetType,
|
"type": assetType,
|
||||||
|
"schematic_id": schematicID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
122
internal/api/v1/handlers_schematic.go
Normal file
122
internal/api/v1/handlers_schematic.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/wild-cloud/wild-central/daemon/internal/assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SchematicGetInstanceSchematic returns the schematic configuration for an instance
|
||||||
|
func (api *API) SchematicGetInstanceSchematic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
instanceName := vars["name"]
|
||||||
|
|
||||||
|
// Validate instance exists
|
||||||
|
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
||||||
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := api.instance.GetInstanceConfigPath(instanceName)
|
||||||
|
|
||||||
|
// Get schematic ID from config
|
||||||
|
schematicID, err := api.config.GetConfigValue(configPath, "cluster.nodes.talos.schematicId")
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get schematic ID: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get version from config
|
||||||
|
version, err := api.config.GetConfigValue(configPath, "cluster.nodes.talos.version")
|
||||||
|
if err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get version: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If schematic is configured, get asset status
|
||||||
|
var assetStatus interface{}
|
||||||
|
if schematicID != "" && schematicID != "null" {
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
status, err := assetsMgr.GetAssetStatus(schematicID)
|
||||||
|
if err == nil {
|
||||||
|
assetStatus = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"schematic_id": schematicID,
|
||||||
|
"version": version,
|
||||||
|
"assets": assetStatus,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchematicUpdateInstanceSchematic updates the schematic configuration for an instance
|
||||||
|
func (api *API) SchematicUpdateInstanceSchematic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
instanceName := vars["name"]
|
||||||
|
|
||||||
|
// Validate instance exists
|
||||||
|
if err := api.instance.ValidateInstance(instanceName); err != nil {
|
||||||
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Instance not found: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
var req struct {
|
||||||
|
SchematicID string `json:"schematic_id"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Download bool `json:"download,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, "Invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.SchematicID == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "schematic_id is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Version == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "version is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := api.instance.GetInstanceConfigPath(instanceName)
|
||||||
|
|
||||||
|
// Update schematic ID in config
|
||||||
|
if err := api.config.SetConfigValue(configPath, "cluster.nodes.talos.schematicId", req.SchematicID); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to set schematic ID: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update version in config
|
||||||
|
if err := api.config.SetConfigValue(configPath, "cluster.nodes.talos.version", req.Version); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to set version: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"message": "Schematic configuration updated successfully",
|
||||||
|
"schematic_id": req.SchematicID,
|
||||||
|
"version": req.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally download assets
|
||||||
|
if req.Download {
|
||||||
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
platform := "amd64" // Default platform
|
||||||
|
if err := assetsMgr.DownloadAssets(req.SchematicID, req.Version, platform, nil); err != nil {
|
||||||
|
response["download_warning"] = fmt.Sprintf("Failed to download assets: %v", err)
|
||||||
|
} else {
|
||||||
|
response["download_status"] = "Assets downloaded successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, response)
|
||||||
|
}
|
||||||
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