package provision import ( "context" "errors" "fmt" "sub2api-cn-relay-manager/internal/host/sub2api" "sub2api-cn-relay-manager/internal/pack" "sub2api-cn-relay-manager/internal/store/sqlite" ) type rollbackHost interface { ListManagedResources(ctx context.Context, req sub2api.ListManagedResourcesRequest) (sub2api.ManagedResourceSnapshot, error) DeleteAccount(ctx context.Context, accountID string) error DeletePlan(ctx context.Context, planID string) error DeleteChannel(ctx context.Context, channelID string) error DeleteGroup(ctx context.Context, groupID string) error } type RollbackRequest struct { Provider pack.ProviderManifest } type RollbackReport struct { AccountsDeleted int PlansDeleted int ChannelsDeleted int GroupsDeleted int } type RollbackService struct { host rollbackHost } func NewRollbackService(host rollbackHost) *RollbackService { return &RollbackService{host: host} } func (s *RollbackService) Rollback(ctx context.Context, req RollbackRequest) (RollbackReport, error) { if s.host == nil { return RollbackReport{}, fmt.Errorf("rollback host is required") } names := SuggestResourceNames(req.Provider) snapshot, err := s.host.ListManagedResources(ctx, sub2api.ListManagedResourcesRequest{ GroupName: names.Group, ChannelName: names.Channel, PlanName: names.Plan, AccountNamePrefix: SuggestAccountNamePrefix(req.Provider), }) if err != nil { return RollbackReport{}, fmt.Errorf("list managed resources: %w", err) } return s.rollbackNamedResources(ctx, snapshot) } func (s *RollbackService) RollbackStoredResources(ctx context.Context, resources []sqlite.ManagedResource) (RollbackReport, error) { if s.host == nil { return RollbackReport{}, fmt.Errorf("rollback host is required") } return s.rollbackNamedResources(ctx, namedResourceSnapshotFromStored(resources)) } func (s *RollbackService) rollbackNamedResources(ctx context.Context, snapshot sub2api.ManagedResourceSnapshot) (RollbackReport, error) { var report RollbackReport var errs []error for index := len(snapshot.Accounts) - 1; index >= 0; index-- { if err := s.host.DeleteAccount(ctx, snapshot.Accounts[index].ID); err != nil { errs = append(errs, fmt.Errorf("delete account %s: %w", snapshot.Accounts[index].ID, err)) continue } report.AccountsDeleted++ } for index := len(snapshot.Plans) - 1; index >= 0; index-- { if err := s.host.DeletePlan(ctx, snapshot.Plans[index].ID); err != nil { errs = append(errs, fmt.Errorf("delete plan %s: %w", snapshot.Plans[index].ID, err)) continue } report.PlansDeleted++ } for index := len(snapshot.Channels) - 1; index >= 0; index-- { if err := s.host.DeleteChannel(ctx, snapshot.Channels[index].ID); err != nil { errs = append(errs, fmt.Errorf("delete channel %s: %w", snapshot.Channels[index].ID, err)) continue } report.ChannelsDeleted++ } for index := len(snapshot.Groups) - 1; index >= 0; index-- { if err := s.host.DeleteGroup(ctx, snapshot.Groups[index].ID); err != nil { errs = append(errs, fmt.Errorf("delete group %s: %w", snapshot.Groups[index].ID, err)) continue } report.GroupsDeleted++ } if len(errs) > 0 { return report, errors.Join(errs...) } return report, nil } func namedResourceSnapshotFromStored(resources []sqlite.ManagedResource) sub2api.ManagedResourceSnapshot { snapshot := sub2api.ManagedResourceSnapshot{} for _, resource := range resources { ref := sub2api.NamedResource{ID: resource.HostResourceID, Name: resource.ResourceName} switch resource.ResourceType { case "account": snapshot.Accounts = append(snapshot.Accounts, ref) case "plan": snapshot.Plans = append(snapshot.Plans, ref) case "channel": snapshot.Channels = append(snapshot.Channels, ref) case "group": snapshot.Groups = append(snapshot.Groups, ref) } } return snapshot }