Add tenants: show who owns what before deciding how to unblock a migration
preflight refuses a layout v0.16 cannot represent, but it reports the problem rather than the shape of the instance around it. Choosing between giving a tenant its own domains, moving accounts, or collapsing everything into one tenant needs the whole picture: which domains have an owner, which accounts reach across to somebody else's, and which of those the converter would have repaired by itself. Read-only, and it names the offending domain against each account that touches it rather than making the reader join the two lists by eye.
This commit is contained in:
@@ -27,6 +27,8 @@ func main() {
|
||||
err = runRehearse(os.Args[2:])
|
||||
case "run":
|
||||
err = runRun(os.Args[2:])
|
||||
case "tenants":
|
||||
err = runTenants(os.Args[2:])
|
||||
case "status":
|
||||
err = runStatus(os.Args[2:])
|
||||
case "report":
|
||||
@@ -49,6 +51,7 @@ commands:
|
||||
preflight run read-only checks and print the migration plan
|
||||
rehearse convert this instance's settings and report what will NOT carry over (read-only)
|
||||
run perform the migration (needs --yes and --recovery-point-confirmed)
|
||||
tenants show which tenant owns which domain, and what blocks a migration (read-only)
|
||||
status show the state of an in-progress or completed run
|
||||
report print the validation report for a run`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// SPDX-FileCopyrightText: 2026 LINUXexpert-org
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/LINUXexpert-org/stalwart-migrator/internal/stalwartapi"
|
||||
)
|
||||
|
||||
// runTenants prints who owns what, read-only.
|
||||
//
|
||||
// preflight already refuses a layout v0.16 cannot represent, but it reports
|
||||
// the problem, not the shape of the instance around it. Deciding how to
|
||||
// resolve one — give a tenant its own domains, or collapse everything into
|
||||
// one — needs the whole picture: which domains have an owner, which accounts
|
||||
// reach across to a domain owned by somebody else, and which of those the
|
||||
// converter would have repaired by itself.
|
||||
func runTenants(args []string) error {
|
||||
fs := flag.NewFlagSet("tenants", flag.ExitOnError)
|
||||
adminURL := fs.String("admin-url", "", "base URL for the instance's admin API (required)")
|
||||
adminUser := fs.String("admin-user", "", "admin username")
|
||||
adminPassword := fs.String("admin-password", os.Getenv("STALWART_MIGRATE_ADMIN_PASSWORD"),
|
||||
"admin password (or set STALWART_MIGRATE_ADMIN_PASSWORD)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *adminURL == "" {
|
||||
return fmt.Errorf("--admin-url is required")
|
||||
}
|
||||
|
||||
client := &stalwartapi.Client{
|
||||
BaseURL: *adminURL, Username: *adminUser, Password: *adminPassword, HTTPClient: &http.Client{},
|
||||
}
|
||||
layout, err := client.FetchTenantLayout(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("read the tenant layout: %w", err)
|
||||
}
|
||||
|
||||
if len(layout.Tenants) == 0 {
|
||||
fmt.Println("single-tenant: this instance declares no tenant principals, so no account can mismatch its domain.")
|
||||
return nil
|
||||
}
|
||||
|
||||
tenants := append([]string(nil), layout.Tenants...)
|
||||
sort.Strings(tenants)
|
||||
fmt.Printf("tenants (%d): %v\n", len(tenants), tenants)
|
||||
|
||||
domains := make([]string, 0, len(layout.DomainTenant))
|
||||
for d := range layout.DomainTenant {
|
||||
domains = append(domains, d)
|
||||
}
|
||||
// Domains that exist only inside an address have no entry of their own.
|
||||
seen := map[string]bool{}
|
||||
for _, p := range layout.Principals {
|
||||
for _, d := range p.Domains {
|
||||
if _, declared := layout.DomainTenant[d]; !declared && !seen[d] {
|
||||
seen[d] = true
|
||||
domains = append(domains, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(domains)
|
||||
|
||||
fmt.Println("\ndomains:")
|
||||
for _, d := range domains {
|
||||
owner, declared := layout.DomainTenant[d]
|
||||
switch {
|
||||
case !declared:
|
||||
fmt.Printf(" %-28s (not declared as a domain principal - the converter infers it, with no tenant)\n", d)
|
||||
case owner == "":
|
||||
fmt.Printf(" %-28s no tenant\n", d)
|
||||
default:
|
||||
fmt.Printf(" %-28s tenant %s\n", d, owner)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\naccounts:")
|
||||
principals := append([]stalwartapi.PrincipalTenancy(nil), layout.Principals...)
|
||||
sort.Slice(principals, func(i, j int) bool { return principals[i].Name < principals[j].Name })
|
||||
for _, p := range principals {
|
||||
tenant := p.Tenant
|
||||
if tenant == "" {
|
||||
tenant = "(global)"
|
||||
}
|
||||
fmt.Printf(" %-32s %-10s tenant %-14s uses %v\n", p.Name, p.Type, tenant, p.Domains)
|
||||
// The rule v0.16 enforces, spelled out per account.
|
||||
for _, d := range p.Domains {
|
||||
owner, declared := layout.DomainTenant[d]
|
||||
if p.Tenant != "" && declared && owner != "" && owner != p.Tenant {
|
||||
fmt.Printf(" ^ %s is owned by tenant %s - v0.16 rejects this\n", d, owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plan := layout.Analyze()
|
||||
fmt.Println("\nwhat happens on migration:")
|
||||
if len(plan.Adoptions) == 0 && len(plan.Problems) == 0 {
|
||||
fmt.Println(" nothing to repair - this layout converts as it stands.")
|
||||
return nil
|
||||
}
|
||||
for _, a := range plan.Adoptions {
|
||||
fmt.Printf(" REPAIRED %s has no tenant of its own and only one tenant's accounts use it; it adopts that tenant\n", a)
|
||||
}
|
||||
for _, p := range plan.Problems {
|
||||
fmt.Printf(" BLOCKED %s: %s\n", p.Domain, p.Detail)
|
||||
}
|
||||
if len(plan.Problems) > 0 {
|
||||
fmt.Println("\nResolve a BLOCKED domain in v0.15 before migrating: give the tenant its own")
|
||||
fmt.Println("domains, or move the accounts involved into the tenant that owns the domain.")
|
||||
fmt.Println("Collapsing every account and domain into a single tenant also resolves it, as")
|
||||
fmt.Println("does removing the tenant principals entirely.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user