Files
felhom-controller/controller/internal/report/config_pull.go
T

116 lines
4.2 KiB
Go

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)
}