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) resp, err := c.HTTPClient.Get(url) if err != nil { return nil, fmt.Errorf("failed to get registry health: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { 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 { return nil, fmt.Errorf("failed to decode health response: %w", err) } 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) // Create multipart form data body := &bytes.Buffer{} writer := multipart.NewWriter(body) // Add metadata metadataJSON, err := json.Marshal(metadata) if err != nil { return nil, fmt.Errorf("failed to marshal metadata: %w", err) } metadataPart, err := writer.CreateFormField("metadata") if err != nil { 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 { return nil, fmt.Errorf("failed to create firmware field: %w", err) } if _, err := io.Copy(firmwarePart, firmwareFile); err != nil { return nil, fmt.Errorf("failed to copy firmware data: %w", err) } writer.Close() req, err := http.NewRequest("POST", url, body) if err != nil { 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 { return nil, fmt.Errorf("failed to upload firmware: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) 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 { return nil, fmt.Errorf("failed to decode upload response: %w", err) } 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) metadataJSON, err := json.Marshal(metadata) if err != nil { return nil, fmt.Errorf("failed to marshal metadata: %w", err) } req, err := http.NewRequest("PUT", url, bytes.NewBuffer(metadataJSON)) if err != nil { 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 { 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) 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 { return nil, fmt.Errorf("failed to decode update response: %w", err) } 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) resp, err := c.HTTPClient.Get(url) if err != nil { return nil, fmt.Errorf("failed to get firmware list: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { 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 { return nil, fmt.Errorf("failed to decode firmware list response: %w", err) } 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) resp, err := c.HTTPClient.Get(url) if err != nil { return nil, fmt.Errorf("failed to download firmware: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("firmware download request failed with status %d", resp.StatusCode) } data, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read firmware data: %w", err) } log.WithFields(log.Fields{ "name": name, "version": version, "size": len(data), }).Info("Downloaded firmware from registry") return data, 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 }