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 }