feat: Add staff instance rename endpoint
This commit is contained in:
@@ -123,6 +123,7 @@ func Routes(cfg config.Config) http.Handler {
|
||||
staff.GET("/subscriptions", staffListSubscriptions)
|
||||
staff.POST("/instances/:id/issue", staffIssue)
|
||||
staff.POST("/instances/:id/relink", staffRelink)
|
||||
staff.PUT("/instances/:id/name", staffRenameInstance)
|
||||
staff.GET("/licenses", staffListLicenses)
|
||||
staff.GET("/plans", staffListPlans)
|
||||
// Plans are keyed on the pair now, so the path is too. A single :tier
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/audit"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/auth"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/cloudprov"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/db"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/licensing"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/models"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
|
||||
sharedmodels "gitea.hostxtra.co.uk/mrhid6/vantage/shared/models"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/shared/provision"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
@@ -618,3 +622,71 @@ func staffCreateAccountUser(c *gin.Context) {
|
||||
Actor: s.Email, Action: "customer_user.created", AccountID: accountID, Target: email})
|
||||
c.JSON(http.StatusCreated, gin.H{"pending": true})
|
||||
}
|
||||
|
||||
// staffRenameInstance renames any instance, with no cooldown.
|
||||
//
|
||||
// It does NOT write renamed_at: a staff rename must not start the customer's
|
||||
// 24h clock, or fixing a name for someone locks them out of fixing it further.
|
||||
//
|
||||
// On self-hosted it changes admin's label only. There is no control-plane row to
|
||||
// write — the install is the customer's — and no slug, because self-hosted has
|
||||
// no tenant subdomain.
|
||||
func staffRenameInstance(c *gin.Context) {
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(body.Name)
|
||||
if name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var inst models.Instance
|
||||
if err := db.Admin("admin_instances").FindOne(ctx,
|
||||
bson.M{"instance_id": c.Param("id")}).Decode(&inst); err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
set := bson.M{"name": name}
|
||||
slug := inst.Slug
|
||||
|
||||
if inst.Deployment == license.DeploymentCloud && !inst.Placeholder {
|
||||
renamed, err := cloudprov.RenameInstance(ctx, inst.InstanceID, name)
|
||||
switch {
|
||||
case errors.Is(err, provision.ErrSlugTaken):
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "that name is already in use"})
|
||||
return
|
||||
case errors.Is(err, provision.ErrNameRejected):
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||
return
|
||||
case err != nil:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
slug = renamed.Slug
|
||||
set["slug"] = renamed.Slug
|
||||
}
|
||||
|
||||
if _, err := db.Admin("admin_instances").UpdateOne(ctx,
|
||||
bson.M{"instance_id": inst.InstanceID}, bson.M{"$set": set}); err != nil {
|
||||
if inst.Deployment == license.DeploymentCloud && !inst.Placeholder {
|
||||
if rbErr := cloudprov.RestoreInstanceIdentity(ctx, inst.InstanceID, inst.Name, inst.Slug); rbErr != nil {
|
||||
log.Printf("staffRenameInstance: rollback of %s failed: %v", inst.InstanceID, rbErr)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
audit.Write(ctx, models.AuditEntry{
|
||||
Actor: auth.Current(c).Email, Action: "instance.renamed", AccountID: inst.AccountID,
|
||||
Target: inst.InstanceID, Detail: inst.Slug + " -> " + slug, IP: c.ClientIP()})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"instance_id": inst.InstanceID, "name": name, "slug": slug})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user