package report import ( "encoding/json" "errors" "fmt" "io" "net/http" "strings" "time" ) // Recovery pull error types for UI display. var ( ErrHubUnreachable = errors.New("hub unreachable") ErrAuthFailed = errors.New("authentication failed") ErrNotFound = errors.New("customer not found") ErrHubError = errors.New("hub error") ) // RecoveryResponse is the combined config + infra backup from the Hub recovery endpoint. type RecoveryResponse struct { CustomerID string `json:"customer_id"` ConfigYAML string `json:"config_yaml"` InfraBackup *InfraBackup `json:"infra_backup"` HasInfraBackup bool `json:"has_infra_backup"` } // PullRecovery fetches combined recovery data from the Hub (config + infra backup). // Auth: X-Retrieval-Password header. func PullRecovery(hubURL, customerID, retrievalPassword string) (*RecoveryResponse, error) { url := strings.TrimRight(hubURL, "/") + "/api/v1/recovery/" + customerID client := &http.Client{Timeout: 30 * time.Second} req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("%w: %v", ErrHubError, err) } req.Header.Set("X-Retrieval-Password", retrievalPassword) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("%w: %v", ErrHubUnreachable, err) } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: // success, continue below case http.StatusUnauthorized: return nil, ErrAuthFailed case http.StatusNotFound: return nil, ErrNotFound default: return nil, fmt.Errorf("%w: HTTP %d", ErrHubError, resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20)) // 10MB limit if err != nil { return nil, fmt.Errorf("%w: reading response: %v", ErrHubError, err) } var rr RecoveryResponse if err := json.Unmarshal(body, &rr); err != nil { return nil, fmt.Errorf("%w: parsing response: %v", ErrHubError, err) } return &rr, nil } // PullConfig fetches a generated controller.yaml from the Hub config endpoint. // Auth: X-Retrieval-Password header. func PullConfig(hubURL, customerID, retrievalPassword string) (string, error) { url := strings.TrimRight(hubURL, "/") + "/api/v1/config/" + customerID client := &http.Client{Timeout: 30 * time.Second} req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return "", fmt.Errorf("%w: %v", ErrHubError, err) } req.Header.Set("X-Retrieval-Password", retrievalPassword) resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("%w: %v", ErrHubUnreachable, err) } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: // success case http.StatusUnauthorized: return "", ErrAuthFailed case http.StatusNotFound: return "", ErrNotFound default: return "", fmt.Errorf("%w: HTTP %d", ErrHubError, resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB limit if err != nil { return "", fmt.Errorf("%w: reading response: %v", ErrHubError, err) } return string(body), nil } // PullInfraBackup fetches the infrastructure backup from the Hub. // Returns nil, nil if no backup exists for this customer. func PullInfraBackup(hubURL, apiKey, customerID string) (*InfraBackup, error) { url := strings.TrimRight(hubURL, "/") + "/api/v1/infra-backup/" + customerID client := &http.Client{Timeout: 30 * time.Second} req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } if apiKey != "" { req.Header.Set("Authorization", "Bearer "+apiKey) } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("hub request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return nil, nil // no backup for this customer } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("hub returned HTTP %d", resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 5<<20)) // 5MB limit if err != nil { return nil, fmt.Errorf("reading response: %w", err) } var ib InfraBackup if err := json.Unmarshal(body, &ib); err != nil { return nil, fmt.Errorf("parsing infra backup: %w", err) } return &ib, nil }