ISOs need version AND schema
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Building the Wild Cloud Central API
|
# 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
|
## Dev Environment Requirements
|
||||||
|
|
||||||
- Go 1.21+
|
- Go 1.21+
|
||||||
|
|||||||
6
go.mod
6
go.mod
@@ -7,3 +7,9 @@ require (
|
|||||||
github.com/rs/cors v1.11.1
|
github.com/rs/cors v1.11.1
|
||||||
gopkg.in/yaml.v3 v3.0.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
|
||||||
|
)
|
||||||
|
|||||||
6
go.sum
6
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 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
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 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -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}/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")
|
||||||
|
|
||||||
// Asset management
|
// PXE Asset management (schematic@version composite key)
|
||||||
r.HandleFunc("/api/v1/assets", api.AssetsListSchematics).Methods("GET")
|
r.HandleFunc("/api/v1/pxe/assets", api.AssetsList).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/assets/{schematicId}", api.AssetsGetSchematic).Methods("GET")
|
r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}", api.AssetsGet).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/assets/{schematicId}/download", api.AssetsDownload).Methods("POST")
|
r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}/download", api.AssetsDownload).Methods("POST")
|
||||||
r.HandleFunc("/api/v1/assets/{schematicId}/pxe/{assetType}", api.AssetsServePXE).Methods("GET")
|
r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}", api.AssetsDelete).Methods("DELETE")
|
||||||
r.HandleFunc("/api/v1/assets/{schematicId}/status", api.AssetsGetStatus).Methods("GET")
|
r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}/pxe/{assetType}", api.AssetsServePXE).Methods("GET")
|
||||||
r.HandleFunc("/api/v1/assets/{schematicId}", api.AssetsDeleteSchematic).Methods("DELETE")
|
r.HandleFunc("/api/v1/pxe/assets/{schematicId}/{version}/status", api.AssetsGetStatus).Methods("GET")
|
||||||
|
|
||||||
// Instance-schematic relationship
|
// Instance-schematic relationship
|
||||||
r.HandleFunc("/api/v1/instances/{name}/schematic", api.SchematicGetInstanceSchematic).Methods("GET")
|
r.HandleFunc("/api/v1/instances/{name}/schematic", api.SchematicGetInstanceSchematic).Methods("GET")
|
||||||
|
|||||||
@@ -11,45 +11,46 @@ import (
|
|||||||
"github.com/wild-cloud/wild-central/daemon/internal/assets"
|
"github.com/wild-cloud/wild-central/daemon/internal/assets"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AssetsListSchematics lists all available schematics
|
// AssetsList lists all available assets (schematic@version combinations)
|
||||||
func (api *API) AssetsListSchematics(w http.ResponseWriter, r *http.Request) {
|
func (api *API) AssetsList(w http.ResponseWriter, r *http.Request) {
|
||||||
assetsMgr := assets.NewManager(api.dataDir)
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
schematics, err := assetsMgr.ListSchematics()
|
assetList, err := assetsMgr.ListAssets()
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"schematics": schematics,
|
"assets": assetList,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssetsGetSchematic returns details for a specific schematic
|
// AssetsGet returns details for a specific asset (schematic@version)
|
||||||
func (api *API) AssetsGetSchematic(w http.ResponseWriter, r *http.Request) {
|
func (api *API) AssetsGet(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
schematicID := vars["schematicId"]
|
schematicID := vars["schematicId"]
|
||||||
|
version := vars["version"]
|
||||||
|
|
||||||
assetsMgr := assets.NewManager(api.dataDir)
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
schematic, err := assetsMgr.GetSchematic(schematicID)
|
asset, err := assetsMgr.GetAsset(schematicID, version)
|
||||||
if err != nil {
|
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
|
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) {
|
func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
schematicID := vars["schematicId"]
|
schematicID := vars["schematicId"]
|
||||||
|
version := vars["version"]
|
||||||
|
|
||||||
// Parse request body
|
// Parse request body
|
||||||
var req struct {
|
var req struct {
|
||||||
Version string `json:"version"`
|
|
||||||
Platform string `json:"platform,omitempty"`
|
Platform string `json:"platform,omitempty"`
|
||||||
AssetTypes []string `json:"asset_types,omitempty"`
|
AssetTypes []string `json:"asset_types,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -59,11 +60,6 @@ func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Version == "" {
|
|
||||||
respondError(w, http.StatusBadRequest, "version is required")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default platform to amd64 if not specified
|
// Default platform to amd64 if not specified
|
||||||
if req.Platform == "" {
|
if req.Platform == "" {
|
||||||
req.Platform = "amd64"
|
req.Platform = "amd64"
|
||||||
@@ -71,7 +67,7 @@ func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Download assets
|
// Download assets
|
||||||
assetsMgr := assets.NewManager(api.dataDir)
|
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))
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to download assets: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -79,7 +75,7 @@ func (api *API) AssetsDownload(w http.ResponseWriter, r *http.Request) {
|
|||||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"message": "Assets downloaded successfully",
|
"message": "Assets downloaded successfully",
|
||||||
"schematic_id": schematicID,
|
"schematic_id": schematicID,
|
||||||
"version": req.Version,
|
"version": version,
|
||||||
"platform": req.Platform,
|
"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) {
|
func (api *API) AssetsServePXE(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
schematicID := vars["schematicId"]
|
schematicID := vars["schematicId"]
|
||||||
|
version := vars["version"]
|
||||||
assetType := vars["assetType"]
|
assetType := vars["assetType"]
|
||||||
|
|
||||||
assetsMgr := assets.NewManager(api.dataDir)
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
// Get asset path
|
// Get asset path
|
||||||
assetPath, err := assetsMgr.GetAssetPath(schematicID, assetType)
|
assetPath, err := assetsMgr.GetAssetPath(schematicID, version, 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
|
||||||
@@ -137,36 +134,39 @@ func (api *API) AssetsServePXE(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
|
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) {
|
func (api *API) AssetsGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
schematicID := vars["schematicId"]
|
schematicID := vars["schematicId"]
|
||||||
|
version := vars["version"]
|
||||||
|
|
||||||
assetsMgr := assets.NewManager(api.dataDir)
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
status, err := assetsMgr.GetAssetStatus(schematicID)
|
status, err := assetsMgr.GetAssetStatus(schematicID, version)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
respondJSON(w, http.StatusOK, status)
|
respondJSON(w, http.StatusOK, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssetsDeleteSchematic deletes a schematic and all its assets
|
// AssetsDelete deletes an asset (schematic@version) and all its files
|
||||||
func (api *API) AssetsDeleteSchematic(w http.ResponseWriter, r *http.Request) {
|
func (api *API) AssetsDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
schematicID := vars["schematicId"]
|
schematicID := vars["schematicId"]
|
||||||
|
version := vars["version"]
|
||||||
|
|
||||||
assetsMgr := assets.NewManager(api.dataDir)
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
|
|
||||||
if err := assetsMgr.DeleteSchematic(schematicID); err != nil {
|
if err := assetsMgr.DeleteAsset(schematicID, version); err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete schematic: %v", err))
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete asset: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
respondJSON(w, http.StatusOK, map[string]string{
|
respondJSON(w, http.StatusOK, map[string]string{
|
||||||
"message": "Schematic deleted successfully",
|
"message": "Asset deleted successfully",
|
||||||
"schematic_id": schematicID,
|
"schematic_id": schematicID,
|
||||||
|
"version": version,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func TestUpdateYAMLFile_NestedStructure(t *testing.T) {
|
|||||||
"cloud": map[string]interface{}{
|
"cloud": map[string]interface{}{
|
||||||
"domain": "test.com",
|
"domain": "test.com",
|
||||||
"dns": map[string]interface{}{
|
"dns": map[string]interface{}{
|
||||||
"ip": "1.2.3.4",
|
"ip": "1.2.3.4",
|
||||||
"port": 53,
|
"port": 53,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -488,8 +488,8 @@ func TestUpdateYAMLFile_UpdateSecrets(t *testing.T) {
|
|||||||
|
|
||||||
// Update secrets
|
// Update secrets
|
||||||
updateData := map[string]interface{}{
|
updateData := map[string]interface{}{
|
||||||
"dbPassword": "secret123",
|
"dbPassword": "secret123",
|
||||||
"apiKey": "key456",
|
"apiKey": "key456",
|
||||||
}
|
}
|
||||||
updateYAML, _ := yaml.Marshal(updateData)
|
updateYAML, _ := yaml.Marshal(updateData)
|
||||||
|
|
||||||
|
|||||||
@@ -45,17 +45,9 @@ func (api *API) PXEListAssets(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
return
|
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{}{
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy to new asset system - serve the file directly
|
respondError(w, http.StatusBadRequest, "This deprecated endpoint requires version. Please use /api/v1/pxe/assets/{schematicId}/{version}/pxe/{assetType}")
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PXEDeleteAsset deletes a PXE asset
|
// PXEDeleteAsset deletes a PXE asset
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ func (api *API) SchematicGetInstanceSchematic(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
// If schematic is configured, get asset status
|
// If schematic is configured, get asset status
|
||||||
var assetStatus interface{}
|
var assetStatus interface{}
|
||||||
if schematicID != "" && schematicID != "null" {
|
if schematicID != "" && schematicID != "null" && version != "" && version != "null" {
|
||||||
assetsMgr := assets.NewManager(api.dataDir)
|
assetsMgr := assets.NewManager(api.dataDir)
|
||||||
status, err := assetsMgr.GetAssetStatus(schematicID)
|
status, err := assetsMgr.GetAssetStatus(schematicID, version)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
assetStatus = status
|
assetStatus = status
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ type EnhancedApp struct {
|
|||||||
|
|
||||||
// RuntimeStatus contains runtime information from kubernetes
|
// RuntimeStatus contains runtime information from kubernetes
|
||||||
type RuntimeStatus struct {
|
type RuntimeStatus struct {
|
||||||
Pods []PodInfo `json:"pods,omitempty"`
|
Pods []PodInfo `json:"pods,omitempty"`
|
||||||
Replicas *ReplicaInfo `json:"replicas,omitempty"`
|
Replicas *ReplicaInfo `json:"replicas,omitempty"`
|
||||||
Resources *ResourceUsage `json:"resources,omitempty"`
|
Resources *ResourceUsage `json:"resources,omitempty"`
|
||||||
RecentEvents []KubernetesEvent `json:"recentEvents,omitempty"`
|
RecentEvents []KubernetesEvent `json:"recentEvents,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ type Asset struct {
|
|||||||
Downloaded bool `json:"downloaded"` // Whether asset exists
|
Downloaded bool `json:"downloaded"` // Whether asset exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schematic represents a Talos schematic and its assets
|
// PXEAsset represents a schematic@version combination and its assets
|
||||||
type Schematic struct {
|
type PXEAsset struct {
|
||||||
SchematicID string `json:"schematic_id"`
|
SchematicID string `json:"schematic_id"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -49,9 +49,10 @@ type AssetStatus struct {
|
|||||||
Complete bool `json:"complete"`
|
Complete bool `json:"complete"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssetDir returns the asset directory for a schematic
|
// GetAssetDir returns the asset directory for a schematic@version composite key
|
||||||
func (m *Manager) GetAssetDir(schematicID string) string {
|
func (m *Manager) GetAssetDir(schematicID, version string) string {
|
||||||
return filepath.Join(m.dataDir, "assets", schematicID)
|
composite := fmt.Sprintf("%s@%s", schematicID, version)
|
||||||
|
return filepath.Join(m.dataDir, "assets", composite)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssetsRootDir returns the root assets directory
|
// GetAssetsRootDir returns the root assets directory
|
||||||
@@ -59,8 +60,8 @@ func (m *Manager) GetAssetsRootDir() string {
|
|||||||
return filepath.Join(m.dataDir, "assets")
|
return filepath.Join(m.dataDir, "assets")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListSchematics returns all available schematics
|
// ListAssets returns all available schematic@version combinations
|
||||||
func (m *Manager) ListSchematics() ([]Schematic, error) {
|
func (m *Manager) ListAssets() ([]PXEAsset, error) {
|
||||||
assetsDir := m.GetAssetsRootDir()
|
assetsDir := m.GetAssetsRootDir()
|
||||||
|
|
||||||
// Ensure assets directory exists
|
// Ensure assets directory exists
|
||||||
@@ -73,52 +74,53 @@ func (m *Manager) ListSchematics() ([]Schematic, error) {
|
|||||||
return nil, fmt.Errorf("reading assets directory: %w", err)
|
return nil, fmt.Errorf("reading assets directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var schematics []Schematic
|
var assets []PXEAsset
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
schematicID := entry.Name()
|
// Parse directory name as schematicID@version
|
||||||
schematic, err := m.GetSchematic(schematicID)
|
parts := strings.SplitN(entry.Name(), "@", 2)
|
||||||
if err != nil {
|
if len(parts) != 2 {
|
||||||
// Skip invalid schematics
|
// Skip invalid directory names (old format or other)
|
||||||
continue
|
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
|
// GetAsset returns details for a specific schematic@version combination
|
||||||
func (m *Manager) GetSchematic(schematicID string) (*Schematic, error) {
|
func (m *Manager) GetAsset(schematicID, version string) (*PXEAsset, error) {
|
||||||
if schematicID == "" {
|
if schematicID == "" {
|
||||||
return nil, fmt.Errorf("schematic ID cannot be empty")
|
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) {
|
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
|
// List assets for this schematic@version
|
||||||
assets, err := m.listSchematicAssets(schematicID)
|
assets, err := m.listAssetFiles(schematicID, version)
|
||||||
if err != nil {
|
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
|
return &PXEAsset{
|
||||||
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,
|
SchematicID: schematicID,
|
||||||
Version: version,
|
Version: version,
|
||||||
Path: assetDir,
|
Path: assetDir,
|
||||||
@@ -126,9 +128,14 @@ func (m *Manager) GetSchematic(schematicID string) (*Schematic, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// listSchematicAssets lists all assets for a schematic
|
// AssetExists checks if a schematic@version exists
|
||||||
func (m *Manager) listSchematicAssets(schematicID string) ([]Asset, error) {
|
func (m *Manager) AssetExists(schematicID, version string) bool {
|
||||||
assetDir := m.GetAssetDir(schematicID)
|
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
|
var assets []Asset
|
||||||
|
|
||||||
@@ -221,19 +228,13 @@ func (m *Manager) DownloadAssets(schematicID, version, platform string, assetTyp
|
|||||||
assetTypes = []string{"kernel", "initramfs", "iso"}
|
assetTypes = []string{"kernel", "initramfs", "iso"}
|
||||||
}
|
}
|
||||||
|
|
||||||
assetDir := m.GetAssetDir(schematicID)
|
assetDir := m.GetAssetDir(schematicID, version)
|
||||||
|
|
||||||
// Ensure asset directory exists
|
// Ensure asset directory exists
|
||||||
if err := storage.EnsureDir(assetDir, 0755); err != nil {
|
if err := storage.EnsureDir(assetDir, 0755); err != nil {
|
||||||
return fmt.Errorf("creating asset directory: %w", err)
|
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
|
// Download each requested asset
|
||||||
for _, assetType := range assetTypes {
|
for _, assetType := range assetTypes {
|
||||||
if err := m.downloadAsset(schematicID, assetType, version, platform); err != nil {
|
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
|
// downloadAsset downloads a single asset
|
||||||
func (m *Manager) downloadAsset(schematicID, assetType, version, platform string) error {
|
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
|
// Determine subdirectory, filename, and URL based on asset type and platform
|
||||||
var subdir, filename, urlPath string
|
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)
|
urlPath = fmt.Sprintf("initramfs-%s.xz", platform)
|
||||||
case "iso":
|
case "iso":
|
||||||
subdir = "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)
|
filename = fmt.Sprintf("talos-%s-metal-%s.iso", version, platform)
|
||||||
urlPath = fmt.Sprintf("metal-%s.iso", platform)
|
urlPath = fmt.Sprintf("metal-%s.iso", platform)
|
||||||
default:
|
default:
|
||||||
@@ -322,31 +323,24 @@ func (m *Manager) downloadAsset(schematicID, assetType, version, platform string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssetStatus returns the download status for a schematic
|
// GetAssetStatus returns the download status for a schematic@version
|
||||||
func (m *Manager) GetAssetStatus(schematicID string) (*AssetStatus, error) {
|
func (m *Manager) GetAssetStatus(schematicID, version string) (*AssetStatus, error) {
|
||||||
if schematicID == "" {
|
if schematicID == "" {
|
||||||
return nil, fmt.Errorf("schematic ID cannot be empty")
|
return nil, fmt.Errorf("schematic ID cannot be empty")
|
||||||
}
|
}
|
||||||
|
if version == "" {
|
||||||
assetDir := m.GetAssetDir(schematicID)
|
return nil, fmt.Errorf("version cannot be empty")
|
||||||
|
|
||||||
// Check if schematic directory exists
|
|
||||||
if !storage.FileExists(assetDir) {
|
|
||||||
return nil, fmt.Errorf("schematic %s not found", schematicID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get version
|
assetDir := m.GetAssetDir(schematicID, version)
|
||||||
version := ""
|
|
||||||
versionPath := filepath.Join(assetDir, "version.txt")
|
// Check if asset directory exists
|
||||||
if storage.FileExists(versionPath) {
|
if !storage.FileExists(assetDir) {
|
||||||
data, err := os.ReadFile(versionPath)
|
return nil, fmt.Errorf("asset %s@%s not found", schematicID, version)
|
||||||
if err == nil {
|
|
||||||
version = strings.TrimSpace(string(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List assets
|
// List assets
|
||||||
assets, err := m.listSchematicAssets(schematicID)
|
assets, err := m.listAssetFiles(schematicID, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("listing assets: %w", err)
|
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
|
// 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 == "" {
|
if schematicID == "" {
|
||||||
return "", fmt.Errorf("schematic ID cannot be empty")
|
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
|
var subdir, pattern string
|
||||||
switch assetType {
|
switch assetType {
|
||||||
@@ -387,7 +384,7 @@ func (m *Manager) GetAssetPath(schematicID, assetType string) (string, error) {
|
|||||||
pattern = "initramfs-amd64.xz"
|
pattern = "initramfs-amd64.xz"
|
||||||
case "iso":
|
case "iso":
|
||||||
subdir = "iso"
|
subdir = "iso"
|
||||||
pattern = "talos-*.iso" // Glob pattern for version-specific filename
|
pattern = "talos-*.iso" // Glob pattern for version and platform-specific filename
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unknown asset type: %s", assetType)
|
return "", fmt.Errorf("unknown asset type: %s", assetType)
|
||||||
}
|
}
|
||||||
@@ -416,13 +413,16 @@ func (m *Manager) GetAssetPath(schematicID, assetType string) (string, error) {
|
|||||||
return assetPath, nil
|
return assetPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSchematic removes a schematic and all its assets
|
// DeleteAsset removes a schematic@version and all its assets
|
||||||
func (m *Manager) DeleteSchematic(schematicID string) error {
|
func (m *Manager) DeleteAsset(schematicID, version string) error {
|
||||||
if schematicID == "" {
|
if schematicID == "" {
|
||||||
return fmt.Errorf("schematic ID cannot be empty")
|
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) {
|
if !storage.FileExists(assetDir) {
|
||||||
return nil // Already deleted, idempotent
|
return nil // Already deleted, idempotent
|
||||||
|
|||||||
@@ -691,10 +691,10 @@ cluster:
|
|||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "creates destination directory",
|
name: "creates destination directory",
|
||||||
srcYAML: `baseDomain: "example.com"`,
|
srcYAML: `baseDomain: "example.com"`,
|
||||||
setupDst: nil,
|
setupDst: nil,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "overwrites existing destination",
|
name: "overwrites existing destination",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func NewManager(dataDir string) *Manager {
|
|||||||
|
|
||||||
// BootstrapProgress tracks detailed bootstrap progress
|
// BootstrapProgress tracks detailed bootstrap progress
|
||||||
type BootstrapProgress struct {
|
type BootstrapProgress struct {
|
||||||
CurrentStep int `json:"current_step"` // 0-6
|
CurrentStep int `json:"current_step"` // 0-6
|
||||||
StepName string `json:"step_name"`
|
StepName string `json:"step_name"`
|
||||||
Attempt int `json:"attempt"`
|
Attempt int `json:"attempt"`
|
||||||
MaxAttempts int `json:"max_attempts"`
|
MaxAttempts int `json:"max_attempts"`
|
||||||
@@ -97,7 +97,6 @@ func (m *Manager) Start(instanceName, opType, target string) (string, error) {
|
|||||||
return opID, nil
|
return opID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// GetByInstance returns an operation for a specific instance
|
// GetByInstance returns an operation for a specific instance
|
||||||
func (m *Manager) GetByInstance(instanceName, opID string) (*Operation, error) {
|
func (m *Manager) GetByInstance(instanceName, opID string) (*Operation, error) {
|
||||||
opsDir := m.GetOperationsDir(instanceName)
|
opsDir := m.GetOperationsDir(instanceName)
|
||||||
|
|||||||
@@ -127,11 +127,11 @@ func TestEnsureDir(t *testing.T) {
|
|||||||
|
|
||||||
func TestReadFile(t *testing.T) {
|
func TestReadFile(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
setup func(tmpDir string) string
|
setup func(tmpDir string) string
|
||||||
wantData []byte
|
wantData []byte
|
||||||
wantErr bool
|
wantErr bool
|
||||||
errCheck func(error) bool
|
errCheck func(error) bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "read existing file",
|
name: "read existing file",
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ func NewKubectl(kubeconfigPath string) *Kubectl {
|
|||||||
|
|
||||||
// PodInfo represents pod information from kubectl
|
// PodInfo represents pod information from kubectl
|
||||||
type PodInfo struct {
|
type PodInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Ready string `json:"ready"`
|
Ready string `json:"ready"`
|
||||||
Restarts int `json:"restarts"`
|
Restarts int `json:"restarts"`
|
||||||
Age string `json:"age"`
|
Age string `json:"age"`
|
||||||
Node string `json:"node,omitempty"`
|
Node string `json:"node,omitempty"`
|
||||||
IP string `json:"ip,omitempty"`
|
IP string `json:"ip,omitempty"`
|
||||||
Containers []ContainerInfo `json:"containers,omitempty"`
|
Containers []ContainerInfo `json:"containers,omitempty"`
|
||||||
Conditions []PodCondition `json:"conditions,omitempty"`
|
Conditions []PodCondition `json:"conditions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainerInfo represents detailed container information
|
// ContainerInfo represents detailed container information
|
||||||
@@ -195,7 +195,7 @@ func (k *Kubectl) GetPods(namespace string, detailed bool) ([]PodInfo, error) {
|
|||||||
Ready bool `json:"ready"`
|
Ready bool `json:"ready"`
|
||||||
RestartCount int `json:"restartCount"`
|
RestartCount int `json:"restartCount"`
|
||||||
State struct {
|
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"`
|
Waiting *struct{ Reason, Message string } `json:"waiting,omitempty"`
|
||||||
Terminated *struct {
|
Terminated *struct {
|
||||||
Reason string
|
Reason string
|
||||||
|
|||||||
@@ -31,22 +31,22 @@ func TestNewTalosctl(t *testing.T) {
|
|||||||
|
|
||||||
func TestTalosconfigBuildArgs(t *testing.T) {
|
func TestTalosconfigBuildArgs(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
talosconfigPath string
|
talosconfigPath string
|
||||||
baseArgs []string
|
baseArgs []string
|
||||||
wantPrefix []string
|
wantPrefix []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no talosconfig adds no prefix",
|
name: "no talosconfig adds no prefix",
|
||||||
talosconfigPath: "",
|
talosconfigPath: "",
|
||||||
baseArgs: []string{"version", "--short"},
|
baseArgs: []string{"version", "--short"},
|
||||||
wantPrefix: nil,
|
wantPrefix: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with talosconfig adds prefix",
|
name: "with talosconfig adds prefix",
|
||||||
talosconfigPath: "/path/to/talosconfig",
|
talosconfigPath: "/path/to/talosconfig",
|
||||||
baseArgs: []string{"version", "--short"},
|
baseArgs: []string{"version", "--short"},
|
||||||
wantPrefix: []string{"--talosconfig", "/path/to/talosconfig"},
|
wantPrefix: []string{"--talosconfig", "/path/to/talosconfig"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,12 +137,12 @@ func TestTalosconfigGenConfig(t *testing.T) {
|
|||||||
|
|
||||||
func TestTalosconfigApplyConfig(t *testing.T) {
|
func TestTalosconfigApplyConfig(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
nodeIP string
|
nodeIP string
|
||||||
configFile string
|
configFile string
|
||||||
insecure bool
|
insecure bool
|
||||||
talosconfigPath string
|
talosconfigPath string
|
||||||
skipTest bool
|
skipTest bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "apply config with all params",
|
name: "apply config with all params",
|
||||||
|
|||||||
@@ -370,9 +370,9 @@ func TestYQExec(t *testing.T) {
|
|||||||
|
|
||||||
func TestCleanYQOutput(t *testing.T) {
|
func TestCleanYQOutput(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "removes trailing newline",
|
name: "removes trailing newline",
|
||||||
|
|||||||
Reference in New Issue
Block a user