docs: mark the admin backend shipped and verified
Ticks all 66 plan steps and records spec 3 as shipped. Also corrects a stale line that still claimed existing cloud tenants are grandfathered by migration 0005. That migration was reverted before plan 2 shipped; those instances are read-only until licensed by hand, and the same file already said so one table above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,7 @@ There is deliberately no automated backfill. Those instances get licensed by han
|
||||
- `config.Config` with `Load() (Config, error)`
|
||||
- `db.Connect(ctx, cfg)`, `db.Admin(name) *mongo.Collection`, `db.Control(name) *mongo.Collection`, `db.EnsureIndexes(ctx)`
|
||||
|
||||
- [ ] **Step 1: Create the module**
|
||||
- [x] **Step 1: Create the module**
|
||||
|
||||
```bash
|
||||
cd c:/Work/Repos/vantage
|
||||
@@ -120,7 +120,7 @@ use (
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the config**
|
||||
- [x] **Step 2: Write the config**
|
||||
|
||||
Create `admin/internal/config/config.go`:
|
||||
|
||||
@@ -237,7 +237,7 @@ func envOr(key, fallback string) string {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write the database layer**
|
||||
- [x] **Step 3: Write the database layer**
|
||||
|
||||
Create `admin/internal/db/db.go`:
|
||||
|
||||
@@ -305,7 +305,7 @@ func Ctx() (context.Context, context.CancelFunc) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Write main.go**
|
||||
- [x] **Step 4: Write main.go**
|
||||
|
||||
Create `admin/cmd/main.go`:
|
||||
|
||||
@@ -371,7 +371,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Write the Dockerfile**
|
||||
- [x] **Step 5: Write the Dockerfile**
|
||||
|
||||
Create `admin/Dockerfile` — context is the repo root, exactly like `server/`:
|
||||
|
||||
@@ -415,7 +415,7 @@ Create `admin/.dockerignore`:
|
||||
*.lic
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Resolve dependencies outside the workspace**
|
||||
- [x] **Step 6: Resolve dependencies outside the workspace**
|
||||
|
||||
```bash
|
||||
cd c:/Work/Repos/vantage
|
||||
@@ -425,7 +425,7 @@ MSYS_NO_PATHCONV=1 docker run --rm -v "$(pwd)":/src -v vantage-gomod:/go/pkg/mod
|
||||
|
||||
`GOWORK=off` is required. In workspace mode `tidy` drops the `require` lines the Docker build needs, and the failure only appears at image build time.
|
||||
|
||||
- [ ] **Step 7: Build**
|
||||
- [x] **Step 7: Build**
|
||||
|
||||
```bash
|
||||
sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
@@ -433,7 +433,7 @@ sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
|
||||
Expected: no output. (`adminctl` does not exist yet — the Dockerfile is not built until Task 11.)
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
- [x] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add admin/ go.work
|
||||
@@ -456,7 +456,7 @@ git commit -m "feat(admin): module skeleton, config and two database connections
|
||||
- `models.SeedPlans(ctx) error`
|
||||
- `db.EnsureIndexes(ctx) error`
|
||||
|
||||
- [ ] **Step 1: Write the documents**
|
||||
- [x] **Step 1: Write the documents**
|
||||
|
||||
Create `admin/internal/models/models.go`:
|
||||
|
||||
@@ -628,7 +628,7 @@ type AuditEntry struct {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the plan seed**
|
||||
- [x] **Step 2: Write the plan seed**
|
||||
|
||||
Create `admin/internal/models/plans.go`:
|
||||
|
||||
@@ -685,7 +685,7 @@ func GetPlan(ctx context.Context, tier string) (*Plan, error) {
|
||||
func now() time.Time { return time.Now().UTC() }
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the indexes**
|
||||
- [x] **Step 3: Add the indexes**
|
||||
|
||||
Append to `admin/internal/db/db.go`:
|
||||
|
||||
@@ -744,7 +744,7 @@ func EnsureIndexes(ctx context.Context) error {
|
||||
|
||||
Add to that file's imports: `"go.mongodb.org/mongo-driver/v2/bson"` and `"go.mongodb.org/mongo-driver/v2/mongo/options"`.
|
||||
|
||||
- [ ] **Step 4: Call both at boot**
|
||||
- [x] **Step 4: Call both at boot**
|
||||
|
||||
In `admin/cmd/main.go`, after the connect block:
|
||||
|
||||
@@ -763,7 +763,7 @@ In `admin/cmd/main.go`, after the connect block:
|
||||
|
||||
Import `"github.com/mrhid6/vantage/admin/internal/models"`.
|
||||
|
||||
- [ ] **Step 5: Build**
|
||||
- [x] **Step 5: Build**
|
||||
|
||||
```bash
|
||||
sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
@@ -771,7 +771,7 @@ sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
- [x] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add admin/
|
||||
@@ -795,7 +795,7 @@ The core of the service. Everything else exists to call this correctly.
|
||||
- `licensing.ErrFreeLimit`, `ErrDeploymentMismatch`, `ErrUnknownTier`
|
||||
- `audit.Write(ctx, e models.AuditEntry)`
|
||||
|
||||
- [ ] **Step 1: Write the audit helper**
|
||||
- [x] **Step 1: Write the audit helper**
|
||||
|
||||
Create `admin/internal/audit/audit.go`:
|
||||
|
||||
@@ -823,7 +823,7 @@ func Write(ctx context.Context, e models.AuditEntry) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write issuance**
|
||||
- [x] **Step 2: Write issuance**
|
||||
|
||||
Create `admin/internal/licensing/issue.go`:
|
||||
|
||||
@@ -1008,7 +1008,7 @@ func checkFreeLimit(ctx context.Context, accountID, exceptInstanceID string) err
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Wire the signing key at boot**
|
||||
- [x] **Step 3: Wire the signing key at boot**
|
||||
|
||||
In `admin/cmd/main.go`, after config loads:
|
||||
|
||||
@@ -1018,7 +1018,7 @@ In `admin/cmd/main.go`, after config loads:
|
||||
|
||||
Import `"github.com/mrhid6/vantage/admin/internal/licensing"`.
|
||||
|
||||
- [ ] **Step 4: Build**
|
||||
- [x] **Step 4: Build**
|
||||
|
||||
```bash
|
||||
sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
@@ -1026,7 +1026,7 @@ sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add admin/
|
||||
@@ -1048,7 +1048,7 @@ git commit -m "feat(admin): licence issuance with plan snapshots and supersessio
|
||||
- `inject.Reconcile(ctx) (checked, repaired int, err error)`
|
||||
- `inject.StartReconciler(ctx)`
|
||||
|
||||
- [ ] **Step 1: Write it**
|
||||
- [x] **Step 1: Write it**
|
||||
|
||||
Create `admin/internal/inject/inject.go`:
|
||||
|
||||
@@ -1205,7 +1205,7 @@ func StartReconciler(ctx context.Context) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Start it at boot**
|
||||
- [x] **Step 2: Start it at boot**
|
||||
|
||||
In `admin/cmd/main.go`, before the HTTP server starts:
|
||||
|
||||
@@ -1217,7 +1217,7 @@ In `admin/cmd/main.go`, before the HTTP server starts:
|
||||
|
||||
Import `"github.com/mrhid6/vantage/admin/internal/inject"`.
|
||||
|
||||
- [ ] **Step 3: Build**
|
||||
- [x] **Step 3: Build**
|
||||
|
||||
```bash
|
||||
sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
@@ -1225,7 +1225,7 @@ sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 4: Confirm the write surface by inspection**
|
||||
- [x] **Step 4: Confirm the write surface by inspection**
|
||||
|
||||
```bash
|
||||
grep -n "Control(" admin/internal/ -r
|
||||
@@ -1233,7 +1233,7 @@ grep -n "Control(" admin/internal/ -r
|
||||
|
||||
Expected: reads of `instances` and `users`, and writes only to `instances` with the three licence fields. Any other write target is a bug — fix it before continuing.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add admin/
|
||||
@@ -1256,7 +1256,7 @@ git commit -m "feat(admin): cloud injection and the 15-minute reconciler"
|
||||
- `auth.HandleStaffLogin`, `auth.HandleLogout`
|
||||
- `adminctl staff-add`
|
||||
|
||||
- [ ] **Step 1: Write sessions**
|
||||
- [x] **Step 1: Write sessions**
|
||||
|
||||
Create `admin/internal/auth/session.go`:
|
||||
|
||||
@@ -1353,7 +1353,7 @@ func ClearCookie(c *gin.Context) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the middleware**
|
||||
- [x] **Step 2: Write the middleware**
|
||||
|
||||
Create `admin/internal/auth/middleware.go`:
|
||||
|
||||
@@ -1418,7 +1418,7 @@ func RequireCustomer() gin.HandlerFunc {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write staff login**
|
||||
- [x] **Step 3: Write staff login**
|
||||
|
||||
Create `admin/internal/auth/staff.go`:
|
||||
|
||||
@@ -1502,7 +1502,7 @@ func HandleLogout(c *gin.Context) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Write the rate limiter**
|
||||
- [x] **Step 4: Write the rate limiter**
|
||||
|
||||
Create `admin/internal/auth/ratelimit.go`:
|
||||
|
||||
@@ -1564,7 +1564,7 @@ func clearAttempts(email string) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Write adminctl staff-add**
|
||||
- [x] **Step 5: Write adminctl staff-add**
|
||||
|
||||
Create `admin/cmd/adminctl/main.go`:
|
||||
|
||||
@@ -1654,7 +1654,7 @@ func fatal(format string, a ...any) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Initialise Redis at boot**
|
||||
- [x] **Step 6: Initialise Redis at boot**
|
||||
|
||||
In `admin/cmd/main.go`, after config loads:
|
||||
|
||||
@@ -1668,7 +1668,7 @@ In `admin/cmd/main.go`, after config loads:
|
||||
pingCancel()
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Build**
|
||||
- [x] **Step 7: Build**
|
||||
|
||||
```bash
|
||||
sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
@@ -1676,7 +1676,7 @@ sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
- [x] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add admin/
|
||||
@@ -1694,7 +1694,7 @@ git commit -m "feat(admin): sessions, staff auth and adminctl"
|
||||
- Consumes: `db.Control("users")`, `db.Control("instances")`, `shared/models`
|
||||
- Produces: `auth.HandleCloudLogin`
|
||||
|
||||
- [ ] **Step 1: Write it**
|
||||
- [x] **Step 1: Write it**
|
||||
|
||||
Create `admin/internal/auth/cloud.go`:
|
||||
|
||||
@@ -1797,7 +1797,7 @@ func HandleCloudLogin(c *gin.Context) {
|
||||
|
||||
Check the field names on `sharedmodels.User` before building — use `UserID`, `Email`, `PasswordHash`, `Role`, `InstanceID` as defined in `shared/models/user.go`, and correct this file if they differ.
|
||||
|
||||
- [ ] **Step 2: Build**
|
||||
- [x] **Step 2: Build**
|
||||
|
||||
```bash
|
||||
sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
@@ -1805,7 +1805,7 @@ sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
|
||||
Expected: no output. (`HandleCustomerLogin` arrives in Task 7 — if you are building tasks in order, stub it as a 501 handler in `customer.go` and replace it there.)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
- [x] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add admin/
|
||||
@@ -1823,7 +1823,7 @@ git commit -m "feat(admin): cloud owner login against the control plane"
|
||||
- Consumes: `db.Admin("customer_users")`, config SMTP
|
||||
- Produces: `auth.HandleCustomerLogin`, `auth.CreateCustomerUser`, `auth.HandleVerify`, `mail.Send`, `mail.SendVerification`, `mail.SendLicense`
|
||||
|
||||
- [ ] **Step 1: Write mail**
|
||||
- [x] **Step 1: Write mail**
|
||||
|
||||
Create `admin/internal/mail/mail.go`:
|
||||
|
||||
@@ -1885,7 +1885,7 @@ func SendLicense(to, instanceName, blob string) error {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write customer auth**
|
||||
- [x] **Step 2: Write customer auth**
|
||||
|
||||
Create `admin/internal/auth/customer.go`:
|
||||
|
||||
@@ -2033,7 +2033,7 @@ func HandleCustomerLogin(c *gin.Context) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Initialise mail at boot**
|
||||
- [x] **Step 3: Initialise mail at boot**
|
||||
|
||||
In `admin/cmd/main.go`:
|
||||
|
||||
@@ -2048,7 +2048,7 @@ In `admin/cmd/main.go`:
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build**
|
||||
- [x] **Step 4: Build**
|
||||
|
||||
```bash
|
||||
sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
@@ -2056,7 +2056,7 @@ sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
|
||||
Expected: no output.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add admin/
|
||||
@@ -2079,7 +2079,7 @@ git commit -m "feat(admin): self-hosted customer accounts with email verificatio
|
||||
- `api.Routes(cfg) http.Handler`
|
||||
- `api.ownedInstance(c, instanceID) (*models.Instance, bool)` — the scoping helper
|
||||
|
||||
- [ ] **Step 1: Write linking and relink**
|
||||
- [x] **Step 1: Write linking and relink**
|
||||
|
||||
Create `admin/internal/licensing/link.go`:
|
||||
|
||||
@@ -2207,7 +2207,7 @@ func Relink(ctx context.Context, accountID, oldID, newID string, staff bool) (*m
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the customer handlers**
|
||||
- [x] **Step 2: Write the customer handlers**
|
||||
|
||||
Create `admin/internal/api/customer.go`:
|
||||
|
||||
@@ -2386,7 +2386,7 @@ func deliver(c *gin.Context, inst *models.Instance, lic *models.License) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write the route table**
|
||||
- [x] **Step 3: Write the route table**
|
||||
|
||||
Create `admin/internal/api/routes.go`:
|
||||
|
||||
@@ -2473,7 +2473,7 @@ func cors(allowed []string) gin.HandlerFunc {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Mount it**
|
||||
- [x] **Step 4: Mount it**
|
||||
|
||||
In `admin/cmd/main.go`, replace `Handler: http.NotFoundHandler()` with:
|
||||
|
||||
@@ -2483,7 +2483,7 @@ In `admin/cmd/main.go`, replace `Handler: http.NotFoundHandler()` with:
|
||||
|
||||
Import `"github.com/mrhid6/vantage/admin/internal/api"`.
|
||||
|
||||
- [ ] **Step 5: Build**
|
||||
- [x] **Step 5: Build**
|
||||
|
||||
```bash
|
||||
sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
@@ -2491,7 +2491,7 @@ sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
|
||||
Expected: failures naming the staff handlers, which arrive in Task 9. Build again after Task 9.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
- [x] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add admin/
|
||||
@@ -2509,7 +2509,7 @@ git commit -m "feat(admin): linking, relink and the scoped customer API"
|
||||
- Consumes: `licensing.Issue`, `licensing.Relink`, `models`
|
||||
- Produces: the handlers named in `routes.go`
|
||||
|
||||
- [ ] **Step 1: Write the handlers**
|
||||
- [x] **Step 1: Write the handlers**
|
||||
|
||||
Create `admin/internal/api/staff.go`:
|
||||
|
||||
@@ -2857,7 +2857,7 @@ func staffInjectionHealth(c *gin.Context) {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build**
|
||||
- [x] **Step 2: Build**
|
||||
|
||||
```bash
|
||||
sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
@@ -2865,7 +2865,7 @@ sh /tmp/gorun.sh admin go build ./... && sh /tmp/gorun.sh admin go vet ./...
|
||||
|
||||
Expected: no output. This is the first build where the whole service compiles.
|
||||
|
||||
- [ ] **Step 3: Audit customer scoping by hand**
|
||||
- [x] **Step 3: Audit customer scoping by hand**
|
||||
|
||||
With no test suite, this replaces spec 3's table-driven test 20.
|
||||
|
||||
@@ -2876,7 +2876,7 @@ grep -n "func \(getAccount\|linkInstance\|relinkInstance\|getInstanceLicense\|do
|
||||
|
||||
Every customer route must either call `ownedInstance` or filter by `s.AccountID`. A route that does neither is a data leak — fix before continuing.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
- [x] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add admin/
|
||||
@@ -2890,7 +2890,7 @@ git commit -m "feat(admin): staff API"
|
||||
**Files:**
|
||||
- Modify: `deploy/docker-compose.site.yml`, `.gitea/workflows/server-deploy.yml`
|
||||
|
||||
- [ ] **Step 1: Add the service**
|
||||
- [x] **Step 1: Add the service**
|
||||
|
||||
In `deploy/docker-compose.site.yml`, alongside `site` and `sitesvc`:
|
||||
|
||||
@@ -2919,11 +2919,11 @@ In `deploy/docker-compose.site.yml`, alongside `site` and `sitesvc`:
|
||||
|
||||
`LICENSE_SIGNING_KEY` appears in exactly one service in exactly one compose file. It must never be added to `server`, and `deploy/docker-compose.yml` — the self-hosted deployment — must not mention admin at all.
|
||||
|
||||
- [ ] **Step 2: Add the build**
|
||||
- [x] **Step 2: Add the build**
|
||||
|
||||
In `.gitea/workflows/server-deploy.yml`, add a fourth image alongside `server`, `web`, `site` and `sitesvc`, with context `.` and file `admin/Dockerfile`, following the pattern the other Go services already use.
|
||||
|
||||
- [ ] **Step 3: Confirm the self-hosted deployment is untouched**
|
||||
- [x] **Step 3: Confirm the self-hosted deployment is untouched**
|
||||
|
||||
```bash
|
||||
grep -c "admin" deploy/docker-compose.yml
|
||||
@@ -2931,7 +2931,7 @@ grep -c "admin" deploy/docker-compose.yml
|
||||
|
||||
Expected: `0`.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
- [x] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add deploy/ .gitea/
|
||||
@@ -2946,7 +2946,7 @@ Everything in containers. With no test suite this is the only evidence.
|
||||
|
||||
**Files:** none
|
||||
|
||||
- [ ] **Step 1: Build every module and the image**
|
||||
- [x] **Step 1: Build every module and the image**
|
||||
|
||||
```bash
|
||||
cd c:/Work/Repos/vantage
|
||||
@@ -2960,7 +2960,7 @@ docker build -q -f server/Dockerfile -t vantage-server:test .
|
||||
|
||||
Expected: all succeed. The image build is the one that catches a `go.sum` the workspace was masking — it caught exactly that in plan 2.
|
||||
|
||||
- [ ] **Step 2: Start the supporting containers**
|
||||
- [x] **Step 2: Start the supporting containers**
|
||||
|
||||
```bash
|
||||
docker run -d --name vadmin-redis -p 6399:6379 redis:7-alpine
|
||||
@@ -2968,7 +2968,7 @@ docker run -d --name vadmin-redis -p 6399:6379 redis:7-alpine
|
||||
|
||||
MongoDB is expected on the host at `27021`, as in plan 2.
|
||||
|
||||
- [ ] **Step 3: Boot the control plane and bootstrap a cloud instance**
|
||||
- [x] **Step 3: Boot the control plane and bootstrap a cloud instance**
|
||||
|
||||
```bash
|
||||
docker run -d --name vadmin-server -p 8080:8080 \
|
||||
@@ -2984,7 +2984,7 @@ curl -s -X POST localhost:8080/auth/bootstrap -H 'Content-Type: application/json
|
||||
|
||||
Record the `instance_id`.
|
||||
|
||||
- [ ] **Step 4: Boot admin**
|
||||
- [x] **Step 4: Boot admin**
|
||||
|
||||
```bash
|
||||
docker run -d --name vadmin -p 8083:8083 \
|
||||
@@ -3008,7 +3008,7 @@ docker run --rm -e ADMIN_MONGO_URI=mongodb://x/y -e CONTROL_MONGO_URI=mongodb://
|
||||
|
||||
Expected: exits non-zero with `missing required environment: LICENSE_SIGNING_KEY`.
|
||||
|
||||
- [ ] **Step 5: Adopt the cloud instance and issue through the staff API**
|
||||
- [x] **Step 5: Adopt the cloud instance and issue through the staff API**
|
||||
|
||||
This is the flow the admin UI will drive when you licence your existing cloud
|
||||
instances by hand.
|
||||
@@ -3043,7 +3043,7 @@ Expected: adopting the instance returns 201, issuing returns 201, and the contro
|
||||
plane reports `"state":"valid"`, `"tier":"professional"` **within 60 seconds,
|
||||
with no restart**. This is the whole system working end to end.
|
||||
|
||||
- [ ] **Step 6: Confirm the Free rule and the deployment check**
|
||||
- [x] **Step 6: Confirm the Free rule and the deployment check**
|
||||
|
||||
Reusing the staff session and `$INSTANCE` from step 5:
|
||||
|
||||
@@ -3054,7 +3054,7 @@ curl -s -X POST localhost:8083/api/staff/instances/$INSTANCE/issue -b /tmp/s.txt
|
||||
|
||||
Expected: `"that plan is not available for this deployment type"` — a self-hosted plan cannot be issued to a cloud instance, and the mirror of that check is what makes Free cloud-only.
|
||||
|
||||
- [ ] **Step 7: Confirm supersession and the snapshot rule**
|
||||
- [x] **Step 7: Confirm supersession and the snapshot rule**
|
||||
|
||||
```bash
|
||||
curl -s -X POST localhost:8083/api/staff/instances/$INSTANCE/issue -b /tmp/s.txt \
|
||||
@@ -3075,7 +3075,7 @@ curl -s "localhost:8083/api/staff/licenses?instance_id=$INSTANCE" -b /tmp/s.txt
|
||||
|
||||
Expected: the existing licences still report `max_servers: -1`. Editing a plan never rewrites history.
|
||||
|
||||
- [ ] **Step 8: Confirm reconciliation repairs a tampered instance**
|
||||
- [x] **Step 8: Confirm reconciliation repairs a tampered instance**
|
||||
|
||||
```bash
|
||||
docker run --rm --add-host host.docker.internal:host-gateway mongo:7 mongosh \
|
||||
@@ -3093,7 +3093,7 @@ curl -s localhost:8080/api/license -b /tmp/a.txt
|
||||
|
||||
Expected: the control plane reports `valid` again once reconciliation has run — the blob is restored from what admin believes it should be. Confirm `reconcile: repairing instance` appears in `docker logs vadmin`.
|
||||
|
||||
- [ ] **Step 9: Confirm customer scoping returns 404, not 403**
|
||||
- [x] **Step 9: Confirm customer scoping returns 404, not 403**
|
||||
|
||||
Create a second account and instance via the staff API, sign in as the first customer, and request the second account's instance:
|
||||
|
||||
@@ -3104,7 +3104,7 @@ curl -s -o /dev/null -w "other account's instance: %{http_code}\n" \
|
||||
|
||||
Expected: **404**. A 403 would confirm the instance exists.
|
||||
|
||||
- [ ] **Step 10: Confirm admin is not a runtime dependency**
|
||||
- [x] **Step 10: Confirm admin is not a runtime dependency**
|
||||
|
||||
```bash
|
||||
docker stop vadmin
|
||||
@@ -3114,7 +3114,7 @@ curl -s -o /dev/null -w "create server: %{http_code}\n" -X POST localhost:8080/a
|
||||
|
||||
Expected: the licence still reports `valid` and mutations still work. **This is the most important check in the plan.** Instances verify offline and must never call admin.
|
||||
|
||||
- [ ] **Step 11: Confirm the control-plane write surface**
|
||||
- [x] **Step 11: Confirm the control-plane write surface**
|
||||
|
||||
```bash
|
||||
docker run --rm --add-host host.docker.internal:host-gateway mongo:7 mongosh \
|
||||
@@ -3124,7 +3124,7 @@ docker run --rm --add-host host.docker.internal:host-gateway mongo:7 mongosh \
|
||||
|
||||
Expected: `servers`, `keys`, `secrets` and every other collection are exactly as the control plane left them. Admin touched only `instances`.
|
||||
|
||||
- [ ] **Step 12: Clean up and commit**
|
||||
- [x] **Step 12: Clean up and commit**
|
||||
|
||||
```bash
|
||||
docker rm -f vadmin vadmin-server vadmin-redis
|
||||
|
||||
@@ -8,9 +8,9 @@ Seven specs, designed 2026-07-24. Build in this order.
|
||||
| 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) | **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 |
|
||||
| 3 | [admin-backend](2026-07-24-admin-backend-design.md) | [plan](../plans/2026-07-24-admin-backend.md) | **shipped**, verified end to end against scratch databases |
|
||||
| 4 | [admin-site](2026-07-24-admin-site-design.md) | — | ready to start |
|
||||
| 5 | [paddle-billing](2026-07-24-paddle-billing-design.md) | — | ready to start |
|
||||
|
||||
Specs 1 and 2 together give working licensing with licences cut by hand with
|
||||
`lkctl` — no admin service needed. 4 and 5 can run in parallel once 3 lands.
|
||||
@@ -66,5 +66,9 @@ Free is cloud-only by construction: it is only ever signed with
|
||||
`deployment: "cloud"`, and verification rejects a deployment mismatch. There is
|
||||
no server-side flag to edit. One Free instance per account.
|
||||
|
||||
**Existing cloud tenants** are grandfathered to Professional, one year out, by
|
||||
migration `0005`.
|
||||
**Existing cloud tenants are not grandfathered.** The migration that would have
|
||||
done it was removed before plan 2 shipped, so every existing cloud instance is
|
||||
read-only until it is licensed by hand through the admin service: attach it to an
|
||||
account with `POST /api/staff/instances`, then `POST /api/staff/instances/:id/issue`.
|
||||
That flow is verified in plan 3, so it works today via the API and is the first
|
||||
job the admin UI is used for.
|
||||
|
||||
Reference in New Issue
Block a user