Files
spore-gateway/pkg/registry/registry.go
2025-10-24 21:45:40 +02:00

515 lines
15 KiB
Go

package registry
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"time"
log "github.com/sirupsen/logrus"
)
// RegistryClient represents a client for communicating with the SPORE registry
type RegistryClient struct {
BaseURL string
HTTPClient *http.Client
}
// NewRegistryClient creates a new registry API client
func NewRegistryClient(baseURL string) *RegistryClient {
return &RegistryClient{
BaseURL: baseURL,
HTTPClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// FirmwareRecord represents a firmware record from the registry
type FirmwareRecord struct {
Name string `json:"name"`
Version string `json:"version"`
Size int64 `json:"size"`
Labels map[string]string `json:"labels"`
Path string `json:"download_url"`
}
// GroupedFirmware represents firmware grouped by name
type GroupedFirmware struct {
Name string `json:"name"`
Firmware []FirmwareRecord `json:"firmware"`
}
// FindFirmwareByNameAndVersion finds firmware in the registry by name and version
func (c *RegistryClient) FindFirmwareByNameAndVersion(name, version string) (*FirmwareRecord, error) {
// Get all firmware from registry
firmwareList, err := c.ListFirmware()
if err != nil {
return nil, fmt.Errorf("failed to list firmware: %w", err)
}
// Search through all firmware groups
for _, group := range firmwareList {
if group.Name == name {
for _, firmware := range group.Firmware {
if firmware.Version == version {
return &firmware, nil
}
}
}
}
return nil, fmt.Errorf("no firmware found with name %s and version %s", name, version)
}
// GetHealth checks the health of the registry
func (c *RegistryClient) GetHealth() (map[string]interface{}, error) {
url := fmt.Sprintf("%s/health", c.BaseURL)
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"endpoint": "/health",
}).Debug("Checking registry health")
resp, err := c.HTTPClient.Get(url)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to check registry health")
return nil, fmt.Errorf("failed to get registry health: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"status_code": resp.StatusCode,
}).Debug("Registry health check returned non-OK status")
return nil, fmt.Errorf("registry health check failed with status %d", resp.StatusCode)
}
var health map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&health); err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to decode health response")
return nil, fmt.Errorf("failed to decode health response: %w", err)
}
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
}).Debug("Successfully checked registry health")
return health, nil
}
// UploadFirmware uploads firmware to the registry
func (c *RegistryClient) UploadFirmware(metadata FirmwareMetadata, firmwareFile io.Reader) (map[string]interface{}, error) {
url := fmt.Sprintf("%s/firmware", c.BaseURL)
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"endpoint": "/firmware",
"name": metadata.Name,
"version": metadata.Version,
}).Debug("Uploading firmware to registry")
// Create multipart form data
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// Add metadata
metadataJSON, err := json.Marshal(metadata)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to marshal firmware metadata")
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
}
metadataPart, err := writer.CreateFormField("metadata")
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to create metadata field")
return nil, fmt.Errorf("failed to create metadata field: %w", err)
}
metadataPart.Write(metadataJSON)
// Add firmware file
firmwarePart, err := writer.CreateFormFile("firmware", fmt.Sprintf("%s-%s.bin", metadata.Name, metadata.Version))
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to create firmware field")
return nil, fmt.Errorf("failed to create firmware field: %w", err)
}
if _, err := io.Copy(firmwarePart, firmwareFile); err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to copy firmware data")
return nil, fmt.Errorf("failed to copy firmware data: %w", err)
}
writer.Close()
req, err := http.NewRequest("POST", url, body)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to create upload request")
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := c.HTTPClient.Do(req)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": metadata.Name,
"version": metadata.Version,
"error": err.Error(),
}).Debug("Failed to upload firmware to registry")
return nil, fmt.Errorf("failed to upload firmware: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": metadata.Name,
"version": metadata.Version,
"status_code": resp.StatusCode,
"error_body": string(body),
}).Debug("Firmware upload returned non-OK status")
return nil, fmt.Errorf("firmware upload failed with status %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to decode upload response")
return nil, fmt.Errorf("failed to decode upload response: %w", err)
}
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": metadata.Name,
"version": metadata.Version,
}).Debug("Successfully uploaded firmware to registry")
return result, nil
}
// UpdateFirmwareMetadata updates firmware metadata in the registry
func (c *RegistryClient) UpdateFirmwareMetadata(name, version string, metadata FirmwareMetadata) (map[string]interface{}, error) {
url := fmt.Sprintf("%s/firmware/%s/%s", c.BaseURL, name, version)
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"endpoint": fmt.Sprintf("/firmware/%s/%s", name, version),
"name": name,
"version": version,
}).Debug("Updating firmware metadata in registry")
metadataJSON, err := json.Marshal(metadata)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"error": err.Error(),
}).Debug("Failed to marshal metadata")
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
}
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(metadataJSON))
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"error": err.Error(),
}).Debug("Failed to create update request")
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"error": err.Error(),
}).Debug("Failed to update firmware metadata in registry")
return nil, fmt.Errorf("failed to update firmware metadata: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"status_code": resp.StatusCode,
"error_body": string(body),
}).Debug("Firmware metadata update returned non-OK status")
return nil, fmt.Errorf("firmware metadata update failed with status %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"error": err.Error(),
}).Debug("Failed to decode update response")
return nil, fmt.Errorf("failed to decode update response: %w", err)
}
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
}).Debug("Successfully updated firmware metadata in registry")
return result, nil
}
// FirmwareMetadata represents firmware metadata for uploads
type FirmwareMetadata struct {
Name string `json:"name"`
Version string `json:"version"`
Labels map[string]string `json:"labels"`
}
// FindFirmwareByLabels finds firmware in the registry that matches the given labels
func (c *RegistryClient) FindFirmwareByLabels(labels map[string]string) (*FirmwareRecord, error) {
// Get all firmware from registry
firmwareList, err := c.ListFirmware()
if err != nil {
return nil, fmt.Errorf("failed to list firmware: %w", err)
}
// Search through all firmware groups
for _, group := range firmwareList {
for _, firmware := range group.Firmware {
if c.firmwareMatchesLabels(firmware.Labels, labels) {
return &firmware, nil
}
}
}
return nil, fmt.Errorf("no firmware found matching labels: %v", labels)
}
// firmwareMatchesLabels checks if firmware labels match the rollout labels
func (c *RegistryClient) firmwareMatchesLabels(firmwareLabels, rolloutLabels map[string]string) bool {
for key, value := range rolloutLabels {
if firmwareValue, exists := firmwareLabels[key]; !exists || firmwareValue != value {
return false
}
}
return true
}
// ListFirmware retrieves all firmware from the registry
func (c *RegistryClient) ListFirmware() ([]GroupedFirmware, error) {
url := fmt.Sprintf("%s/firmware", c.BaseURL)
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"endpoint": "/firmware",
}).Debug("Fetching firmware list from registry")
resp, err := c.HTTPClient.Get(url)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to fetch firmware list from registry")
return nil, fmt.Errorf("failed to get firmware list: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"status_code": resp.StatusCode,
}).Debug("Firmware list request returned non-OK status")
return nil, fmt.Errorf("firmware list request failed with status %d", resp.StatusCode)
}
var firmwareList []GroupedFirmware
if err := json.NewDecoder(resp.Body).Decode(&firmwareList); err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"error": err.Error(),
}).Debug("Failed to decode firmware list response")
return nil, fmt.Errorf("failed to decode firmware list response: %w", err)
}
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"firmware_count": len(firmwareList),
}).Debug("Successfully fetched firmware list from registry")
return firmwareList, nil
}
// DownloadFirmware downloads firmware binary from the registry
func (c *RegistryClient) DownloadFirmware(name, version string) ([]byte, error) {
url := fmt.Sprintf("%s/firmware/%s/%s", c.BaseURL, name, version)
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"endpoint": fmt.Sprintf("/firmware/%s/%s", name, version),
"name": name,
"version": version,
}).Debug("Downloading firmware from registry")
resp, err := c.HTTPClient.Get(url)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"error": err.Error(),
}).Debug("Failed to download firmware from registry")
return nil, fmt.Errorf("failed to download firmware: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"status_code": resp.StatusCode,
}).Debug("Firmware download request returned non-OK status")
return nil, fmt.Errorf("firmware download request failed with status %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"error": err.Error(),
}).Debug("Failed to read firmware data from registry")
return nil, fmt.Errorf("failed to read firmware data: %w", err)
}
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"size": len(data),
}).Debug("Successfully downloaded firmware from registry")
return data, nil
}
// DeleteFirmware deletes firmware from the registry
func (c *RegistryClient) DeleteFirmware(name, version string) (map[string]interface{}, error) {
url := fmt.Sprintf("%s/firmware/%s/%s", c.BaseURL, name, version)
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"endpoint": fmt.Sprintf("/firmware/%s/%s", name, version),
"name": name,
"version": version,
}).Debug("Deleting firmware from registry")
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"error": err.Error(),
}).Debug("Failed to create delete request")
return nil, fmt.Errorf("failed to create delete request: %w", err)
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"error": err.Error(),
}).Debug("Failed to delete firmware from registry")
return nil, fmt.Errorf("failed to delete firmware: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"status_code": resp.StatusCode,
"error_body": string(body),
}).Debug("Firmware delete returned non-OK status")
return nil, fmt.Errorf("firmware delete request failed with status %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
"error": err.Error(),
}).Debug("Failed to decode delete response")
return nil, fmt.Errorf("failed to decode delete response: %w", err)
}
log.WithFields(log.Fields{
"registry_url": c.BaseURL,
"name": name,
"version": version,
}).Debug("Successfully deleted firmware from registry")
return result, nil
}
// HealthCheck checks if the registry is healthy
func (c *RegistryClient) HealthCheck() error {
url := fmt.Sprintf("%s/health", c.BaseURL)
resp, err := c.HTTPClient.Get(url)
if err != nil {
return fmt.Errorf("failed to check registry health: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("registry health check failed with status %d", resp.StatusCode)
}
return nil
}