feat: infra backup retention + version picker
Hub: GFS retention (7d/4w/3m, ~14 versions) in new infra_backup_versions table. Recovery endpoint supports ?version=ID. New /versions API endpoint. Dashboard shows backup history. Controller: local drive backups rotated into history/ (last 5 versions). Setup wizard shows version picker for Hub restores when multiple versions exist. Scan results enriched with app names, disk count, history badge. Local restore supports historical versions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,12 +18,22 @@ var (
|
||||
ErrHubError = errors.New("hub error")
|
||||
)
|
||||
|
||||
// BackupVersionSummary holds metadata about one backup version (from Hub).
|
||||
type BackupVersionSummary struct {
|
||||
ID int64 `json:"id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
StackCount int `json:"stack_count"`
|
||||
DiskCount int `json:"disk_count"`
|
||||
StackNames []string `json:"stack_names,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
CustomerID string `json:"customer_id"`
|
||||
ConfigYAML string `json:"config_yaml"`
|
||||
InfraBackup *InfraBackup `json:"infra_backup"`
|
||||
HasInfraBackup bool `json:"has_infra_backup"`
|
||||
BackupVersions []BackupVersionSummary `json:"backup_versions,omitempty"`
|
||||
}
|
||||
|
||||
// PullRecovery fetches combined recovery data from the Hub (config + infra backup).
|
||||
@@ -69,6 +79,48 @@ func PullRecovery(hubURL, customerID, retrievalPassword string) (*RecoveryRespon
|
||||
return &rr, nil
|
||||
}
|
||||
|
||||
// PullRecoveryVersion fetches recovery data for a specific backup version ID.
|
||||
func PullRecoveryVersion(hubURL, customerID, retrievalPassword string, versionID int64) (*RecoveryResponse, error) {
|
||||
url := strings.TrimRight(hubURL, "/") + "/api/v1/recovery/" + customerID + fmt.Sprintf("?version=%d", versionID)
|
||||
|
||||
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
|
||||
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))
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user