hub v0.61.0 + felhom-tenantsync v1.1.0: Customer RESET (middle lifecycle tier)
One operator action returns a customer to pre-first-install: all operational state dies (offsite repo, PBS namespace+backups, DR recipe, one-time secret, claim state, retained escrow custody); identity + basic config + provenance + events survive. Sits between host delete and customer Delete. - store/customer_reset.go: customer_resets journal, live inventory, ack-gated purge (never touches identity/provenance/events), DeleteClaim. - claim.ResetToUnclaimed: delete claim row -> fresh code next onboarding. - offsite.Deprovision (idempotent) + OffsiteIdentifier + ClearProvisionedDescriptor. - tenantsync.Deprovision + felhom-tenantsync.sh deprovision op (destroys ns + backup groups + token; shared user untouched; idempotent). - web/customer_reset.go: GET reset -> inventory JSON; POST -> orchestration (external teardown FIRST, DB purge LAST; refuse-while-hosts; typed-id + separate escrow ack). Amber RESET card distinct from red Danger-zone Delete. - Red-proofs: ack-gate + partial-failure resumability (both proven red); store ack-gating + journal round-trip; offsite idempotency + descriptor clear; RESET-card render. Green: build + vet + test.
This commit is contained in:
@@ -99,6 +99,31 @@ func (c *Client) Reissue(ctx context.Context, customerID string) (*Result, error
|
||||
return c.tenancyOp(ctx, "reissue", customerID)
|
||||
}
|
||||
|
||||
// Deprovision DESTROYS the customer's PBS namespace, all its backup groups, and its token — the
|
||||
// customer-RESET teardown (v0.61.0, operator ack-gated). The shared felhom@pbs user is never touched
|
||||
// (co-tenants ride it). Idempotent: a missing tenant is a clean success (existed=false). This carries
|
||||
// NO secret, so it does not route through tenancyOp's token-field validation.
|
||||
func (c *Client) Deprovision(ctx context.Context, customerID string) (existed bool, err error) {
|
||||
if !customerIDRe.MatchString(customerID) {
|
||||
return false, fmt.Errorf("tenantsync: invalid customer_id %q", customerID)
|
||||
}
|
||||
payload, err := json.Marshal(map[string]string{"op": "deprovision", "customer_id": customerID})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
stdout, stderr, runErr := c.exec(ctx, payload)
|
||||
resp, err := parseResponse(stdout, stderr, runErr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if resp.Namespace == "" {
|
||||
return false, fmt.Errorf("tenantsync: deprovision response missing namespace")
|
||||
}
|
||||
c.logger.Printf("[INFO] tenantsync: deprovision ok for %s (ns=%s, existed=%t)",
|
||||
customerID, resp.Namespace, resp.Deleted)
|
||||
return resp.Deleted, nil
|
||||
}
|
||||
|
||||
// Fingerprint returns the endpoint PBS's API cert fingerprint (the descriptor field).
|
||||
func (c *Client) Fingerprint(ctx context.Context) (string, error) {
|
||||
stdout, stderr, runErr := c.exec(ctx, []byte(`{"op":"fingerprint"}`))
|
||||
@@ -136,9 +161,10 @@ func (c *Client) tenancyOp(ctx context.Context, op, customerID string) (*Result,
|
||||
|
||||
// response is the script's stdout contract — ok carries the Result fields, error carries code+error.
|
||||
type response struct {
|
||||
Status string `json:"status"`
|
||||
Code string `json:"code"`
|
||||
Error string `json:"error"`
|
||||
Status string `json:"status"`
|
||||
Code string `json:"code"`
|
||||
Error string `json:"error"`
|
||||
Deleted bool `json:"deleted"` // deprovision op: whether the namespace existed (was destroyed)
|
||||
Result
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user