From c623843d532b56812eea0f574b16336b84be445c Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Sat, 8 Nov 2025 22:23:26 +0000 Subject: [PATCH] ISOs need version AND schema --- BUILDING_WILD_API.md | 6 ++ go.mod | 6 ++ go.sum | 6 ++ internal/api/v1/handlers.go | 14 +-- internal/api/v1/handlers_assets.go | 56 +++++----- internal/api/v1/handlers_config_test.go | 6 +- internal/api/v1/handlers_pxe.go | 27 +---- internal/api/v1/handlers_schematic.go | 4 +- internal/apps/models.go | 6 +- internal/assets/assets.go | 136 ++++++++++++------------ internal/config/manager_test.go | 8 +- internal/operations/operations.go | 3 +- internal/storage/storage_test.go | 10 +- internal/tools/kubectl.go | 20 ++-- internal/tools/talosctl_test.go | 30 +++--- internal/tools/yq_test.go | 6 +- 16 files changed, 170 insertions(+), 174 deletions(-) diff --git a/BUILDING_WILD_API.md b/BUILDING_WILD_API.md index 746bf70..d1eb7c8 100644 --- a/BUILDING_WILD_API.md +++ b/BUILDING_WILD_API.md @@ -1,5 +1,11 @@ # Building the Wild Cloud Central API +These are instructions for working with the Wild Cloud Central API (Wild API). Wild API is a web service that runs on Wild Central. Users can interact with the API directly, through the Wild CLI, or through the Wild Web App. The CLI and Web App depend on the API extensively. + +Whenever changes are made to the API, it is important that the CLI and API are updated appropriately. + +Use tests on the API extensively to keep the API functioning well for all clients, but don't duplicate test layers. If something is tested in one place, it doesn't need to be tested again in another place. Prefer unit tests. Tests should be run with `make test` after all API changes. If a bug was found by any means other than tests, it is a signal that a test should have been present to catch it earlier, so make sure a new test catches that bug before fixing it. + ## Dev Environment Requirements - Go 1.21+ diff --git a/go.mod b/go.mod index 0cadc9f..3d52c14 100644 --- a/go.mod +++ b/go.mod @@ -7,3 +7,9 @@ require ( github.com/rs/cors v1.11.1 gopkg.in/yaml.v3 v3.0.1 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect +) diff --git a/go.sum b/go.sum index 9a17f03..d609a59 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/api/v1/handlers.go b/internal/api/v1/handlers.go index cc75058..8068241 100644 --- a/internal/api/v1/handlers.go +++ b/internal/api/v1/handlers.go @@ -98,13 +98,13 @@ 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}", api.NodeDelete).Methods("DELETE") - // Asset management - r.HandleFunc("/api/v1/assets", api.AssetsListSchematics).Methods("GET") - r.HandleFunc("/api/v1/assets/{schematicId}", api.AssetsGetSchematic).Methods("GET") - r.HandleFunc("/api/v1/assets/{schematicId}/download", api.AssetsDownload).Methods("POST") - 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") + // PXE Asset management (schematic@version composite key) + r.HandleFunc("/api/v1/pxe/assets", api.AssetsList).Methods("GET") + r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}", api.AssetsGet).Methods("GET") + r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}/download", api.AssetsDownload).Methods("POST") + r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}", api.AssetsDelete).Methods("DELETE") + r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}/pxe/{assetType}", api.AssetsServePXE).Methods("GET") + r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}/status", api.AssetsGetStatus).Methods("GET") // Instance-schematic relationship r.HandleFunc("/api/v1/instances/{name}/schematic", api.SchematicGetInstanceSchematic).Methods("GET") diff --git a/internal/api/v1/handlers_assets.go b/internal/api/v1/handlers_assets.go index e374eb4..f9a9960 100644 --- a/internal/api/v1/handlers_assets.go +++ b/internal/api/v1/handlers_assets.go @@ -11,45 +11,46 @@ import ( "github.com/wild-cloud/wild-central/daemon/internal/assets" ) -// AssetsListSchematics lists all available schematics -func (api *API) AssetsListSchematics(w http.ResponseWriter, r *http.Request) { +// AssetsList lists all available assets (schematic@version combinations) +func (api *API) AssetsList(w http.ResponseWriter, r *http.Request) { assetsMgr := assets.NewManager(api.dataDir) - schematics, err := assetsMgr.ListSchematics() + assetList, err := assetsMgr.ListAssets() if err != nil { - respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list schematics: %v", err)) + respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list assets: %v", err)) return } respondJSON(w, http.StatusOK, map[string]interface{}{ - "schematics": schematics, + "assets": assetList, }) } -// AssetsGetSchematic returns details for a specific schematic -func (api *API) AssetsGetSchematic(w http.ResponseWriter, r *http.Request) { +// AssetsGet returns details for a specific asset (schematic@version) +func (api *API) AssetsGet(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) schematicID := vars["schematicId"] + version := vars["version"] assetsMgr := assets.NewManager(api.dataDir) - schematic, err := assetsMgr.GetSchematic(schematicID) + asset, err := assetsMgr.GetAsset(schematicID, version) if err != nil { - respondError(w, http.StatusNotFound, fmt.Sprintf("Schematic not found: %v", err)) + respondError(w, http.StatusNotFound, fmt.Sprintf("Asset not found: %v", err)) return } - respondJSON(w, http.StatusOK, schematic) + respondJSON(w, http.StatusOK, asset) } -// AssetsDownload downloads assets for a schematic +// AssetsDownload downloads assets for a schematic@version func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) schematicID := vars["schematicId"] + version := vars["version"] // Parse request body var req struct { - Version string `json:"version"` Platform string `json:"platform,omitempty"` AssetTypes []string `json:"asset_types,omitempty"` } @@ -59,11 +60,6 @@ func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) { 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" @@ -71,7 +67,7 @@ func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) { // Download assets assetsMgr := assets.NewManager(api.dataDir) - if err := assetsMgr.DownloadAssets(schematicID, req.Version, req.Platform, req.AssetTypes); err != nil { + if err := assetsMgr.DownloadAssets(schematicID, version, req.Platform, req.AssetTypes); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to download assets: %v", err)) return } @@ -79,7 +75,7 @@ func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) { respondJSON(w, http.StatusOK, map[string]interface{}{ "message": "Assets downloaded successfully", "schematic_id": schematicID, - "version": req.Version, + "version": version, "platform": req.Platform, }) } @@ -88,12 +84,13 @@ func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) { func (api *API) AssetsServePXE(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) schematicID := vars["schematicId"] + version := vars["version"] assetType := vars["assetType"] assetsMgr := assets.NewManager(api.dataDir) // Get asset path - assetPath, err := assetsMgr.GetAssetPath(schematicID, assetType) + assetPath, err := assetsMgr.GetAssetPath(schematicID, version, assetType) if err != nil { respondError(w, http.StatusNotFound, fmt.Sprintf("Asset not found: %v", err)) return @@ -137,36 +134,39 @@ func (api *API) AssetsServePXE(w http.ResponseWriter, r *http.Request) { http.ServeContent(w, r, info.Name(), info.ModTime(), file) } -// AssetsGetStatus returns download status for a schematic +// AssetsGetStatus returns download status for a schematic@version func (api *API) AssetsGetStatus(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) schematicID := vars["schematicId"] + version := vars["version"] assetsMgr := assets.NewManager(api.dataDir) - status, err := assetsMgr.GetAssetStatus(schematicID) + status, err := assetsMgr.GetAssetStatus(schematicID, version) if err != nil { - respondError(w, http.StatusNotFound, fmt.Sprintf("Schematic not found: %v", err)) + respondError(w, http.StatusNotFound, fmt.Sprintf("Asset 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) { +// AssetsDelete deletes an asset (schematic@version) and all its files +func (api *API) AssetsDelete(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) schematicID := vars["schematicId"] + version := vars["version"] assetsMgr := assets.NewManager(api.dataDir) - if err := assetsMgr.DeleteSchematic(schematicID); err != nil { - respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete schematic: %v", err)) + if err := assetsMgr.DeleteAsset(schematicID, version); err != nil { + respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete asset: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{ - "message": "Schematic deleted successfully", + "message": "Asset deleted successfully", "schematic_id": schematicID, + "version": version, }) } diff --git a/internal/api/v1/handlers_config_test.go b/internal/api/v1/handlers_config_test.go index 547f382..a69fc4d 100644 --- a/internal/api/v1/handlers_config_test.go +++ b/internal/api/v1/handlers_config_test.go @@ -177,7 +177,7 @@ func TestUpdateYAMLFile_NestedStructure(t *testing.T) { "cloud": map[string]interface{}{ "domain": "test.com", "dns": map[string]interface{}{ - "ip": "1.2.3.4", + "ip": "1.2.3.4", "port": 53, }, }, @@ -488,8 +488,8 @@ func TestUpdateYAMLFile_UpdateSecrets(t *testing.T) { // Update secrets updateData := map[string]interface{}{ - "dbPassword": "secret123", - "apiKey": "key456", + "dbPassword": "secret123", + "apiKey": "key456", } updateYAML, _ := yaml.Marshal(updateData) diff --git a/internal/api/v1/handlers_pxe.go b/internal/api/v1/handlers_pxe.go index 6e79f08..9350d6e 100644 --- a/internal/api/v1/handlers_pxe.go +++ b/internal/api/v1/handlers_pxe.go @@ -45,17 +45,9 @@ func (api *API) PXEListAssets(w http.ResponseWriter, r *http.Request) { }) return } - - // Proxy to new asset system - 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, map[string]interface{}{ - "assets": schematic.Assets, + "assets": []interface{}{}, + "message": "Please use the new /api/v1/pxe/assets endpoint with both schematic ID and version", }) } @@ -184,20 +176,7 @@ func (api *API) PXEGetAsset(w http.ResponseWriter, r *http.Request) { return } - // Proxy to new asset system - serve the file directly - assetsMgr := assets.NewManager(api.dataDir) - assetPath, err := assetsMgr.GetAssetPath(schematicID, assetType) - if err != nil { - respondError(w, http.StatusNotFound, fmt.Sprintf("Asset not found: %v", err)) - return - } - - respondJSON(w, http.StatusOK, map[string]interface{}{ - "type": assetType, - "path": assetPath, - "valid": true, - "schematic_id": schematicID, - }) + respondError(w, http.StatusBadRequest, "This deprecated endpoint requires version. Please use /api/v1/pxe/assets/{schematicId}/{version}/pxe/{assetType}") } // PXEDeleteAsset deletes a PXE asset diff --git a/internal/api/v1/handlers_schematic.go b/internal/api/v1/handlers_schematic.go index ada3616..021a43f 100644 --- a/internal/api/v1/handlers_schematic.go +++ b/internal/api/v1/handlers_schematic.go @@ -39,9 +39,9 @@ func (api *API) SchematicGetInstanceSchematic(w http.ResponseWriter, r *http.Req // If schematic is configured, get asset status var assetStatus interface{} - if schematicID != "" && schematicID != "null" { + if schematicID != "" && schematicID != "null" && version != "" && version != "null" { assetsMgr := assets.NewManager(api.dataDir) - status, err := assetsMgr.GetAssetStatus(schematicID) + status, err := assetsMgr.GetAssetStatus(schematicID, version) if err == nil { assetStatus = status } diff --git a/internal/apps/models.go b/internal/apps/models.go index 69c584e..7b5f59f 100644 --- a/internal/apps/models.go +++ b/internal/apps/models.go @@ -37,9 +37,9 @@ type EnhancedApp struct { // RuntimeStatus contains runtime information from kubernetes type RuntimeStatus struct { - Pods []PodInfo `json:"pods,omitempty"` - Replicas *ReplicaInfo `json:"replicas,omitempty"` - Resources *ResourceUsage `json:"resources,omitempty"` + Pods []PodInfo `json:"pods,omitempty"` + Replicas *ReplicaInfo `json:"replicas,omitempty"` + Resources *ResourceUsage `json:"resources,omitempty"` RecentEvents []KubernetesEvent `json:"recentEvents,omitempty"` } diff --git a/internal/assets/assets.go b/internal/assets/assets.go index bb13b19..ba77bb5 100644 --- a/internal/assets/assets.go +++ b/internal/assets/assets.go @@ -33,8 +33,8 @@ type Asset struct { Downloaded bool `json:"downloaded"` // Whether asset exists } -// Schematic represents a Talos schematic and its assets -type Schematic struct { +// 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"` @@ -49,9 +49,10 @@ type AssetStatus struct { 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) +// 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 @@ -59,8 +60,8 @@ func (m *Manager) GetAssetsRootDir() string { return filepath.Join(m.dataDir, "assets") } -// ListSchematics returns all available schematics -func (m *Manager) ListSchematics() ([]Schematic, error) { +// ListAssets returns all available schematic@version combinations +func (m *Manager) ListAssets() ([]PXEAsset, error) { assetsDir := m.GetAssetsRootDir() // Ensure assets directory exists @@ -73,52 +74,53 @@ func (m *Manager) ListSchematics() ([]Schematic, error) { return nil, fmt.Errorf("reading assets directory: %w", err) } - var schematics []Schematic + var assets []PXEAsset for _, entry := range entries { if entry.IsDir() { - schematicID := entry.Name() - schematic, err := m.GetSchematic(schematicID) - if err != nil { - // Skip invalid schematics + // 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 } - schematics = append(schematics, *schematic) + schematicID := parts[0] + version := parts[1] + + asset, err := m.GetAsset(schematicID, version) + if err != nil { + // Skip invalid assets + continue + } + assets = append(assets, *asset) } } - return schematics, nil + return assets, nil } -// GetSchematic returns details for a specific schematic -func (m *Manager) GetSchematic(schematicID string) (*Schematic, error) { +// 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) + assetDir := m.GetAssetDir(schematicID, version) - // Check if schematic directory exists + // Check if asset directory exists if !storage.FileExists(assetDir) { - return nil, fmt.Errorf("schematic %s not found", schematicID) + return nil, fmt.Errorf("asset %s@%s not found", schematicID, version) } - // List assets for this schematic - assets, err := m.listSchematicAssets(schematicID) + // List assets for this schematic@version + assets, err := m.listAssetFiles(schematicID, version) if err != nil { - return nil, fmt.Errorf("listing schematic assets: %w", err) + return nil, fmt.Errorf("listing 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{ + return &PXEAsset{ SchematicID: schematicID, Version: version, Path: assetDir, @@ -126,9 +128,14 @@ func (m *Manager) GetSchematic(schematicID string) (*Schematic, error) { }, nil } -// listSchematicAssets lists all assets for a schematic -func (m *Manager) listSchematicAssets(schematicID string) ([]Asset, error) { - assetDir := m.GetAssetDir(schematicID) +// 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 @@ -221,19 +228,13 @@ func (m *Manager) DownloadAssets(schematicID, version, platform string, assetTyp assetTypes = []string{"kernel", "initramfs", "iso"} } - assetDir := m.GetAssetDir(schematicID) + 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) } - // 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 { @@ -246,7 +247,7 @@ func (m *Manager) DownloadAssets(schematicID, version, platform string, assetTyp // downloadAsset downloads a single asset func (m *Manager) downloadAsset(schematicID, assetType, version, platform string) error { - assetDir := m.GetAssetDir(schematicID) + assetDir := m.GetAssetDir(schematicID, version) // Determine subdirectory, filename, and URL based on asset type and platform var subdir, filename, urlPath string @@ -261,7 +262,7 @@ func (m *Manager) downloadAsset(schematicID, assetType, version, platform string urlPath = fmt.Sprintf("initramfs-%s.xz", platform) case "iso": subdir = "iso" - // Preserve version and platform in filename for clarity + // Include version in filename for clarity filename = fmt.Sprintf("talos-%s-metal-%s.iso", version, platform) urlPath = fmt.Sprintf("metal-%s.iso", platform) default: @@ -322,31 +323,24 @@ func (m *Manager) downloadAsset(schematicID, assetType, version, platform string return nil } -// GetAssetStatus returns the download status for a schematic -func (m *Manager) GetAssetStatus(schematicID string) (*AssetStatus, error) { +// 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") } - - assetDir := m.GetAssetDir(schematicID) - - // Check if schematic directory exists - if !storage.FileExists(assetDir) { - return nil, fmt.Errorf("schematic %s not found", schematicID) + if version == "" { + return nil, fmt.Errorf("version cannot be empty") } - // 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)) - } + 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.listSchematicAssets(schematicID) + assets, err := m.listAssetFiles(schematicID, version) if err != nil { return nil, fmt.Errorf("listing assets: %w", err) } @@ -370,12 +364,15 @@ func (m *Manager) GetAssetStatus(schematicID string) (*AssetStatus, error) { } // GetAssetPath returns the path to a specific asset file -func (m *Manager) GetAssetPath(schematicID, assetType string) (string, error) { +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) + assetDir := m.GetAssetDir(schematicID, version) var subdir, pattern string switch assetType { @@ -387,7 +384,7 @@ func (m *Manager) GetAssetPath(schematicID, assetType string) (string, error) { pattern = "initramfs-amd64.xz" case "iso": subdir = "iso" - pattern = "talos-*.iso" // Glob pattern for version-specific filename + pattern = "talos-*.iso" // Glob pattern for version and platform-specific filename default: return "", fmt.Errorf("unknown asset type: %s", assetType) } @@ -416,13 +413,16 @@ func (m *Manager) GetAssetPath(schematicID, assetType string) (string, error) { return assetPath, nil } -// DeleteSchematic removes a schematic and all its assets -func (m *Manager) DeleteSchematic(schematicID string) error { +// 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) + assetDir := m.GetAssetDir(schematicID, version) if !storage.FileExists(assetDir) { return nil // Already deleted, idempotent diff --git a/internal/config/manager_test.go b/internal/config/manager_test.go index b898a1e..d50d30c 100644 --- a/internal/config/manager_test.go +++ b/internal/config/manager_test.go @@ -691,10 +691,10 @@ cluster: wantErr: false, }, { - name: "creates destination directory", - srcYAML: `baseDomain: "example.com"`, - setupDst: nil, - wantErr: false, + name: "creates destination directory", + srcYAML: `baseDomain: "example.com"`, + setupDst: nil, + wantErr: false, }, { name: "overwrites existing destination", diff --git a/internal/operations/operations.go b/internal/operations/operations.go index 8db6a31..278b11d 100644 --- a/internal/operations/operations.go +++ b/internal/operations/operations.go @@ -28,7 +28,7 @@ func NewManager(dataDir string) *Manager { // BootstrapProgress tracks detailed bootstrap progress type BootstrapProgress struct { - CurrentStep int `json:"current_step"` // 0-6 + CurrentStep int `json:"current_step"` // 0-6 StepName string `json:"step_name"` Attempt int `json:"attempt"` MaxAttempts int `json:"max_attempts"` @@ -97,7 +97,6 @@ func (m *Manager) Start(instanceName, opType, target string) (string, error) { return opID, nil } - // GetByInstance returns an operation for a specific instance func (m *Manager) GetByInstance(instanceName, opID string) (*Operation, error) { opsDir := m.GetOperationsDir(instanceName) diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go index fcce005..d49fe0b 100644 --- a/internal/storage/storage_test.go +++ b/internal/storage/storage_test.go @@ -127,11 +127,11 @@ func TestEnsureDir(t *testing.T) { func TestReadFile(t *testing.T) { tests := []struct { - name string - setup func(tmpDir string) string - wantData []byte - wantErr bool - errCheck func(error) bool + name string + setup func(tmpDir string) string + wantData []byte + wantErr bool + errCheck func(error) bool }{ { name: "read existing file", diff --git a/internal/tools/kubectl.go b/internal/tools/kubectl.go index 24c58a6..a992290 100644 --- a/internal/tools/kubectl.go +++ b/internal/tools/kubectl.go @@ -26,15 +26,15 @@ func NewKubectl(kubeconfigPath string) *Kubectl { // PodInfo represents pod information from kubectl type PodInfo struct { - Name string `json:"name"` - Status string `json:"status"` - Ready string `json:"ready"` - Restarts int `json:"restarts"` - Age string `json:"age"` - Node string `json:"node,omitempty"` - IP string `json:"ip,omitempty"` - Containers []ContainerInfo `json:"containers,omitempty"` - Conditions []PodCondition `json:"conditions,omitempty"` + Name string `json:"name"` + Status string `json:"status"` + Ready string `json:"ready"` + Restarts int `json:"restarts"` + Age string `json:"age"` + Node string `json:"node,omitempty"` + IP string `json:"ip,omitempty"` + Containers []ContainerInfo `json:"containers,omitempty"` + Conditions []PodCondition `json:"conditions,omitempty"` } // ContainerInfo represents detailed container information @@ -195,7 +195,7 @@ func (k *Kubectl) GetPods(namespace string, detailed bool) ([]PodInfo, error) { Ready bool `json:"ready"` RestartCount int `json:"restartCount"` State struct { - Running *struct{ StartedAt time.Time } `json:"running,omitempty"` + Running *struct{ StartedAt time.Time } `json:"running,omitempty"` Waiting *struct{ Reason, Message string } `json:"waiting,omitempty"` Terminated *struct { Reason string diff --git a/internal/tools/talosctl_test.go b/internal/tools/talosctl_test.go index cfc31e3..ad939a0 100644 --- a/internal/tools/talosctl_test.go +++ b/internal/tools/talosctl_test.go @@ -31,22 +31,22 @@ func TestNewTalosctl(t *testing.T) { func TestTalosconfigBuildArgs(t *testing.T) { tests := []struct { - name string + name string talosconfigPath string - baseArgs []string - wantPrefix []string + baseArgs []string + wantPrefix []string }{ { - name: "no talosconfig adds no prefix", + name: "no talosconfig adds no prefix", talosconfigPath: "", - baseArgs: []string{"version", "--short"}, - wantPrefix: nil, + baseArgs: []string{"version", "--short"}, + wantPrefix: nil, }, { - name: "with talosconfig adds prefix", + name: "with talosconfig adds prefix", talosconfigPath: "/path/to/talosconfig", - baseArgs: []string{"version", "--short"}, - wantPrefix: []string{"--talosconfig", "/path/to/talosconfig"}, + baseArgs: []string{"version", "--short"}, + wantPrefix: []string{"--talosconfig", "/path/to/talosconfig"}, }, } @@ -137,12 +137,12 @@ func TestTalosconfigGenConfig(t *testing.T) { func TestTalosconfigApplyConfig(t *testing.T) { tests := []struct { - name string - nodeIP string - configFile string - insecure bool - talosconfigPath string - skipTest bool + name string + nodeIP string + configFile string + insecure bool + talosconfigPath string + skipTest bool }{ { name: "apply config with all params", diff --git a/internal/tools/yq_test.go b/internal/tools/yq_test.go index f16e29c..ff9b919 100644 --- a/internal/tools/yq_test.go +++ b/internal/tools/yq_test.go @@ -370,9 +370,9 @@ func TestYQExec(t *testing.T) { func TestCleanYQOutput(t *testing.T) { tests := []struct { - name string - input string - want string + name string + input string + want string }{ { name: "removes trailing newline",