Compare commits

..
4 Commits
Author SHA1 Message Date
mrhid6 c4f1684304 docs(plan): drop backfill, add staff instance attach
Server Deploy / deploy (push) Failing after 37s
Existing cloud instances get licensed by hand through the admin UI instead of
an automated backfill. That needs POST /api/staff/instances, which nothing else
provided — without it there is no way to attach an existing cloud instance to
an account.
2026-07-24 16:08:29 +01:00
mrhid6 8ed3bec511 docs: mark plans 1 and 2 shipped 2026-07-24 16:00:45 +01:00
mrhid6 58faf2e57f docs: add the admin-backend implementation plan 2026-07-24 16:00:37 +01:00
mrhid6 8e5f35f40c revert(server): drop the licence grandfather migration
Instances stay read-only until a licence is set. No licence is generated by
the control plane, which keeps the signing key out of it entirely.
2026-07-24 15:49:46 +01:00
4 changed files with 3165 additions and 112 deletions
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -6,9 +6,9 @@ Seven specs, designed 2026-07-24. Build in this order.
|---|---|---|---|
| 0a | [shared-module](2026-07-24-shared-module-design.md) | [plan](../plans/2026-07-24-shared-module.md) | **shipped** |
| 0b | [instance-rename](2026-07-24-instance-rename-design.md) | [plan](../plans/2026-07-24-instance-rename.md) | **shipped**, migration verified on live |
| 1 | [licensing-core](2026-07-24-licensing-core-design.md) | [plan](../plans/2026-07-24-licensing-core.md) | planned |
| 2 | [instance-licensing](2026-07-24-instance-licensing-design.md) | [plan](../plans/2026-07-24-instance-licensing.md) | planned |
| 3 | [admin-backend](2026-07-24-admin-backend-design.md) | — | blocks 4 and 5 |
| 1 | [licensing-core](2026-07-24-licensing-core-design.md) | [plan](../plans/2026-07-24-licensing-core.md) | **shipped** |
| 2 | [instance-licensing](2026-07-24-instance-licensing-design.md) | [plan](../plans/2026-07-24-instance-licensing.md) | **shipped**, no grandfathering — existing cloud instances are read-only until admin backfills |
| 3 | [admin-backend](2026-07-24-admin-backend-design.md) | [plan](../plans/2026-07-24-admin-backend.md) | planned |
| 4 | [admin-site](2026-07-24-admin-site-design.md) | — | needs 3 |
| 5 | [paddle-billing](2026-07-24-paddle-billing-design.md) | — | needs 3 |
-7
View File
@@ -59,13 +59,6 @@ func main() {
log.Fatalf("scoped collection check failed: %v", assertErr)
}
gfCtx, gfCancel := context.WithTimeout(context.Background(), 2*time.Minute)
gfErr := services.MigrateGrandfatherLicences(gfCtx, db.Database)
gfCancel()
if gfErr != nil {
log.Fatalf("licence grandfather migration failed: %v", gfErr)
}
if err := services.EnsureAuthIndexes(); err != nil {
log.Fatalf("failed to ensure auth indexes: %v", err)
}
-102
View File
@@ -1,102 +0,0 @@
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
}