feat: Add instance rename to shared provisioning
This commit is contained in:
@@ -79,6 +79,92 @@ func CreateInstanceWithID(ctx context.Context, db *mongo.Database, instanceID, n
|
||||
return nil, fmt.Errorf("%w: could not find a free slug for %q", ErrNameRejected, name)
|
||||
}
|
||||
|
||||
// ErrSlugTaken means the slug a new name derives to already belongs to another
|
||||
// instance.
|
||||
//
|
||||
// Rename refuses rather than appending a counter the way creation does. Creation
|
||||
// appends because the customer is waiting on an instance and any free slug will
|
||||
// do; a rename is a request for one specific host, and silently landing them on
|
||||
// "acme-2" answers a question they did not ask.
|
||||
var ErrSlugTaken = errors.New("slug taken")
|
||||
|
||||
// RenameSlug derives the slug a rename to name would move an instance to, given
|
||||
// the slug it holds now.
|
||||
//
|
||||
// It returns the current slug unchanged when the name still derives to it, so a
|
||||
// cosmetic edit — capitalisation, punctuation, a trailing "Ltd." — is not a move
|
||||
// and cannot collide with the instance's own slug.
|
||||
func RenameSlug(name, currentSlug string) (string, error) {
|
||||
base, err := BaseSlug(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %s", ErrNameRejected, err.Error())
|
||||
}
|
||||
if base == currentSlug {
|
||||
return currentSlug, nil
|
||||
}
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// RenameInstance changes an instance's name and re-derives its slug from it.
|
||||
//
|
||||
// The count-then-update is racy on its own, and is safe for the same reason
|
||||
// CreateInstanceWithID's loop is: instances.slug carries a unique index, so a
|
||||
// lost race surfaces as a duplicate-key error. Unlike creation there is nothing
|
||||
// to retry with — the caller asked for one specific name — so it becomes
|
||||
// ErrSlugTaken. Do not remove the duplicate-key branch, and do not remove the
|
||||
// index.
|
||||
func RenameInstance(ctx context.Context, db *mongo.Database, instanceID, name string) (*models.Instance, error) {
|
||||
var inst models.Instance
|
||||
if err := db.Collection("instances").FindOne(ctx,
|
||||
bson.M{"instance_id": instanceID}).Decode(&inst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slug, err := RenameSlug(name, inst.Slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if slug != inst.Slug {
|
||||
n, err := db.Collection("instances").CountDocuments(ctx, bson.M{
|
||||
"slug": slug,
|
||||
"instance_id": bson.M{"$ne": instanceID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n > 0 {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSlugTaken, slug)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.Collection("instances").UpdateOne(ctx,
|
||||
bson.M{"instance_id": instanceID},
|
||||
bson.M{"$set": bson.M{"name": name, "slug": slug}}); err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSlugTaken, slug)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inst.Name = name
|
||||
inst.Slug = slug
|
||||
return &inst, nil
|
||||
}
|
||||
|
||||
// RestoreInstanceIdentity writes an exact name and slug back, unwinding a rename
|
||||
// whose caller-side bookkeeping then failed.
|
||||
//
|
||||
// It derives nothing. The values being restored may include a creation-time
|
||||
// collision suffix that no name derives to, so re-running RenameInstance with the
|
||||
// old name would not reproduce them.
|
||||
func RestoreInstanceIdentity(ctx context.Context, db *mongo.Database, instanceID, name, slug string) error {
|
||||
_, err := db.Collection("instances").UpdateOne(ctx,
|
||||
bson.M{"instance_id": instanceID},
|
||||
bson.M{"$set": bson.M{"name": name, "slug": slug}})
|
||||
return err
|
||||
}
|
||||
|
||||
// RollbackInstance deletes an instance that has no users.
|
||||
//
|
||||
// It refuses an instance that has users. Rollback exists to clean up a
|
||||
|
||||
Reference in New Issue
Block a user