From e02b26305475aa1082e547c374bc229b6b665cc9 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Wed, 12 Aug 2026 10:06:31 +0000 Subject: [PATCH] feat: Add rename cooldown field and cloudprov rename --- admin/internal/cloudprov/cloudprov.go | 17 +++++++++++++++++ admin/internal/models/models.go | 14 ++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/admin/internal/cloudprov/cloudprov.go b/admin/internal/cloudprov/cloudprov.go index d516d2b..9824aeb 100644 --- a/admin/internal/cloudprov/cloudprov.go +++ b/admin/internal/cloudprov/cloudprov.go @@ -202,3 +202,20 @@ func ProjectedUsers(ctx context.Context, hqUserID string) ([]sharedmodels.User, } return users, nil } + +// RenameInstance changes a cloud instance's name and moves it to the slug that +// name derives to. +// +// It writes `instances` and nothing else, so admin's control-plane write +// boundary is unchanged. It issues no licence: a licence binds the instance +// UUID, which a rename never touches. +func RenameInstance(ctx context.Context, instanceID, name string) (*sharedmodels.Instance, error) { + return provision.RenameInstance(ctx, db.ControlDB(), instanceID, name) +} + +// RestoreInstanceIdentity puts an instance's previous name and slug back, for a +// caller unwinding a rename whose admin-side write failed. Leaving the two +// databases disagreeing would have HQ print a host that is not the host. +func RestoreInstanceIdentity(ctx context.Context, instanceID, name, slug string) error { + return provision.RestoreInstanceIdentity(ctx, db.ControlDB(), instanceID, name, slug) +} diff --git a/admin/internal/models/models.go b/admin/internal/models/models.go index af2b5f2..c12bcfb 100644 --- a/admin/internal/models/models.go +++ b/admin/internal/models/models.go @@ -111,6 +111,15 @@ const GracePeriod = 3 * 24 * time.Hour // second mechanism. const RenewWindow = 7 * 24 * time.Hour +// RenameCooldown is how long a customer must wait between renames of one +// instance. +// +// A rename moves the instance's DNS host and invalidates every saved link to it, +// so this exists to make that a considered act rather than a slider. Staff are +// not subject to it: a support conversation about a name is already a human +// deciding. +const RenameCooldown = 24 * time.Hour + type Account struct { ID bson.ObjectID `bson:"_id,omitempty" json:"-"` AccountID string `bson:"account_id" json:"account_id"` @@ -137,6 +146,11 @@ type Instance struct { Status string `bson:"status" json:"status"` CurrentLicense string `bson:"current_license,omitempty" json:"current_license,omitempty"` RelinkCount int `bson:"relink_count" json:"relink_count"` + // RenamedAt is when this instance last changed name, and backs the customer + // rename cooldown. It is a pointer because absent means "never renamed"; a + // zero time.Time would read as year 1 — an inert cooldown, but only by + // accident. Staff renames deliberately leave it alone. + RenamedAt *time.Time `bson:"renamed_at,omitempty" json:"renamed_at,omitempty"` InjectFailedAt *time.Time `bson:"inject_failed_at,omitempty" json:"inject_failed_at,omitempty"` // NoticesSent holds the lifecycle notice keys already emailed for the // CURRENT term ("expiring", "expired", "delete_7", "delete_1"). Renewal