feat(server): grandfather existing cloud instances onto Professional

This commit is contained in:
2026-07-24 15:26:26 +01:00
parent 8626898e5e
commit 09a39090c6
2 changed files with 109 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
package services
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
"github.com/mrhid6/vantage/shared/license"
"github.com/mrhid6/vantage/shared/models"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
)
// MigrateGrandfatherLicences stores pre-issued licences on instances that have
// none. Cloud only.
//
// The blobs are supplied through VANTAGE_GRANDFATHER_BLOBS, a JSON object
// mapping instance_id to licence blob, because this process cannot sign: it
// holds no private key and the signing code is compiled out. Cut the blobs
// beforehand with lkctl.
//
// The variable is single-use. Unset it on the next deploy.
func MigrateGrandfatherLicences(ctx context.Context, db *mongo.Database) error {
const marker = "0005_grandfather_licences"
if n, _ := db.Collection("migrations").CountDocuments(ctx, bson.M{"_id": marker}); n > 0 {
return nil
}
if DeploymentMode() != license.DeploymentCloud {
log.Printf("0005: not a cloud deployment, skipping")
return nil
}
raw := os.Getenv("VANTAGE_GRANDFATHER_BLOBS")
if raw == "" {
log.Printf("0005: VANTAGE_GRANDFATHER_BLOBS not set, skipping (no marker recorded)")
return nil
}
var blobs map[string]string
if err := json.Unmarshal([]byte(raw), &blobs); err != nil {
return fmt.Errorf("0005: VANTAGE_GRANDFATHER_BLOBS is not valid JSON: %w", err)
}
cur, err := db.Collection("instances").Find(ctx, bson.M{
"$or": []bson.M{
{"license_blob": bson.M{"$exists": false}},
{"license_blob": ""},
},
})
if err != nil {
return fmt.Errorf("0005: list instances: %w", err)
}
var instances []models.Instance
if err := cur.All(ctx, &instances); err != nil {
return fmt.Errorf("0005: decode instances: %w", err)
}
var stored, missing int
for _, inst := range instances {
blob, ok := blobs[inst.InstanceID]
if !ok || blob == "" {
log.Printf("0005: no blob supplied for instance %s (%s)", inst.InstanceID, inst.Slug)
missing++
continue
}
res := license.Verify(blob, license.VerifyOpts{
InstanceID: inst.InstanceID,
Deployment: license.DeploymentCloud,
})
if res.State == license.StateInvalid {
return fmt.Errorf("0005: blob for instance %s is rejected: %s", inst.InstanceID, res.Reason)
}
if _, err := db.Collection("instances").UpdateOne(ctx,
bson.M{"instance_id": inst.InstanceID},
bson.M{"$set": bson.M{
"license_blob": blob,
"license_tier": res.License.Tier,
"license_expiry": res.License.ExpiresAt,
}}); err != nil {
return fmt.Errorf("0005: store blob for %s: %w", inst.InstanceID, err)
}
log.Printf("0005: stored %s licence for instance %s (%s), expires %s",
res.License.Tier, inst.InstanceID, inst.Slug,
res.License.ExpiresAt.Format(time.RFC3339))
stored++
}
if missing > 0 {
return fmt.Errorf("0005: %d instance(s) had no blob supplied; issue them with lkctl and rerun", missing)
}
_, err = db.Collection("migrations").InsertOne(ctx,
bson.M{"_id": marker, "applied_at": time.Now()})
log.Printf("0005: grandfathered %d instance(s)", stored)
return err
}