package report import ( "errors" "fmt" "io" "net/http" "strings" "time" ) // Config-pull error types for setup-wizard UI display. // // These (and PullConfig) were previously part of infra_pull.go alongside the // disk-tier DR-recovery (PullRecovery / infra-backup) client. Disk recovery moved // to the host agent in slice 8C; only the fresh-install config download survives // here, so it lives in this slimmed file. var ( ErrHubUnreachable = errors.New("hub unreachable") ErrAuthFailed = errors.New("authentication failed") ErrNotFound = errors.New("customer not found") ErrHubError = errors.New("hub error") ) // PullConfig fetches a generated controller.yaml from the Hub config endpoint. // Auth: X-Retrieval-Password header. Used by the setup wizard's fresh-install flow. 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 } // ErrBoxConfigUnsupported: the hub has no GET /box-config (a hub before v0.161.0) — the caller falls back to the // passphrase pull. var ErrBoxConfigUnsupported = errors.New("hub has no box-config route") // PullBoxConfig fetches the generated controller.yaml with the box's OWN hub key (GET /api/v1/box-config/{id}, hub ≥ // v0.161.0; R-1159). A live box refreshes its config this way, so the hub can refuse the household's retrieval // passphrase once the box is live (hub `api/passphrase_scope.go`). func PullBoxConfig(hubURL, customerID, apiKey string) (string, error) { url := strings.TrimRight(hubURL, "/") + "/api/v1/box-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("Authorization", "Bearer "+apiKey) 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: case http.StatusUnauthorized: return "", ErrAuthFailed case http.StatusNotFound, http.StatusMethodNotAllowed: return "", ErrBoxConfigUnsupported default: return "", fmt.Errorf("%w: HTTP %d", ErrHubError, resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return "", fmt.Errorf("%w: reading response: %v", ErrHubError, err) } return string(body), nil } // PullConfigPreferKey is the LIVE box's config refresh (report.ConfigRefresher): with the box's own key first; the // retrieval passphrase only when the hub has no box-config route (a hub before v0.161.0) or the box holds no key yet. // Any other answer to the key pull is returned as it is — never retried with the passphrase (R-1159: the passphrase // is for a box that has no key of its own). Pinned by TestR1159_RefreshUsesTheBoxKey. func PullConfigPreferKey(hubURL, customerID, apiKey, retrievalPassword string, logf func(string, ...any)) (string, error) { if apiKey != "" { y, err := PullBoxConfig(hubURL, customerID, apiKey) if !errors.Is(err, ErrBoxConfigUnsupported) { if err == nil { logf("[INFO] [config] refreshed controller.yaml with the box's own hub key (box-config)") } return y, err } logf("[INFO] [config] the hub has no box-config route (hub < 0.161.0) — refreshing with the retrieval passphrase") } return PullConfig(hubURL, customerID, retrievalPassword) }