mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-15 03:09:06 +02:00
[management] Keep embedded IdP deployments on a single account (#7380)
This commit is contained in:
@@ -50,6 +50,7 @@ The build requires `CGO_ENABLED=1` because it links the SQLite driver used by `S
|
||||
| `--domain` | string | `""` | Sets both dashboard and API domain (convenience shorthand) |
|
||||
| `--dashboard-domain` | string | *(required)* | Dashboard domain (for redirect URIs) |
|
||||
| `--api-domain` | string | *(required)* | API domain (for Dex issuer and callback URLs) |
|
||||
| `--single-account-mode-domain` | string | `netbird.selfhosted` | Domain single account mode groups users under. Used only when the account has no domain of its own; passing one that conflicts with the account's existing domain is an error |
|
||||
| `--dry-run` | bool | `false` | Preview changes without writing |
|
||||
| `--force` | bool | `false` | Skip interactive confirmation prompt |
|
||||
| `--skip-config` | bool | `false` | Skip config generation (DB-only migration) |
|
||||
@@ -68,6 +69,7 @@ All flags can be overridden via environment variables. Env vars take precedence
|
||||
| `NETBIRD_CONFIG_PATH` | `--config` |
|
||||
| `NETBIRD_DATA_DIR` | `--datadir` |
|
||||
| `NETBIRD_IDP_SEED_INFO` | `--idp-seed-info` |
|
||||
| `NETBIRD_SINGLE_ACCOUNT_MODE_DOMAIN` | `--single-account-mode-domain` |
|
||||
| `NETBIRD_DRY_RUN` | `--dry-run` (set to `"true"`) |
|
||||
| `NETBIRD_FORCE` | `--force` (set to `"true"`) |
|
||||
| `NETBIRD_SKIP_CONFIG` | `--skip-config` (set to `"true"`) |
|
||||
|
||||
@@ -6,16 +6,18 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server/idp/migration"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
|
||||
type migrationConfig struct {
|
||||
// Data
|
||||
dashboardURL string
|
||||
apiURL string
|
||||
configPath string
|
||||
dataDir string
|
||||
idpSeedInfo string
|
||||
dashboardURL string
|
||||
apiURL string
|
||||
configPath string
|
||||
dataDir string
|
||||
idpSeedInfo string
|
||||
singleAccountDomain string
|
||||
|
||||
// Options
|
||||
dryRun bool
|
||||
@@ -51,6 +53,7 @@ func configFromArgs(args []string) (*migrationConfig, error) {
|
||||
fs.StringVar(&cfg.configPath, "config", "", "path to management.json (required)")
|
||||
fs.StringVar(&cfg.dataDir, "datadir", "", "override data directory from config")
|
||||
fs.StringVar(&cfg.idpSeedInfo, "idp-seed-info", "", "base64-encoded connector JSON (overrides auto-detection)")
|
||||
fs.StringVar(&cfg.singleAccountDomain, "single-account-mode-domain", "", "domain single account mode groups users under, used only when the account has no domain of its own (default "+migration.DefaultSingleAccountDomain+")")
|
||||
fs.BoolVar(&cfg.dryRun, "dry-run", false, "preview changes without writing")
|
||||
fs.BoolVar(&cfg.force, "force", false, "skip confirmation prompt")
|
||||
fs.BoolVar(&cfg.skipConfig, "skip-config", false, "skip config generation (DB migration only)")
|
||||
@@ -118,6 +121,10 @@ func applyOverrides(cfg *migrationConfig, domain string) {
|
||||
cfg.idpSeedInfo = val
|
||||
}
|
||||
|
||||
if val, ok := os.LookupEnv("NETBIRD_SINGLE_ACCOUNT_MODE_DOMAIN"); ok {
|
||||
cfg.singleAccountDomain = val
|
||||
}
|
||||
|
||||
// Enforce dry run if any value is provided
|
||||
if sval, ok := os.LookupEnv("NETBIRD_DRY_RUN"); ok {
|
||||
if val, err := strconv.ParseBool(sval); err == nil {
|
||||
@@ -170,5 +177,9 @@ func validateConfig(cfg *migrationConfig) error {
|
||||
return fmt.Errorf("--dashboard-domain is required")
|
||||
}
|
||||
|
||||
if _, err := migration.NormalizeSingleAccountDomain(cfg.singleAccountDomain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -71,6 +71,10 @@ func run(cfg *migrationConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := preflightAccounts(cfg, mgmtConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !cfg.skipPopulateUserInfo {
|
||||
err := populateUserInfoFromIDP(cfg, mgmtConfig)
|
||||
if err != nil {
|
||||
@@ -102,6 +106,22 @@ func run(cfg *migrationConfig) error {
|
||||
return generateConfig(cfg, connectorConfig)
|
||||
}
|
||||
|
||||
func preflightAccounts(cfg *migrationConfig, mgmtConfig *nbconfig.Config) error {
|
||||
ctx := context.Background()
|
||||
migStore, migEventStore, cleanup, err := openStores(ctx, mgmtConfig, cfg.dataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
srv := &migrationServer{store: migStore, eventStore: migEventStore}
|
||||
if err := migration.RequireSingleAccount(srv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return migration.CheckSingleAccountDomain(srv, cfg.singleAccountDomain)
|
||||
}
|
||||
|
||||
// validateSchema opens the store and checks that all required tables and columns
|
||||
// exist. If anything is missing, it returns a descriptive error telling the user
|
||||
// to upgrade their management server.
|
||||
@@ -224,6 +244,8 @@ func migrateDB(cfg *migrationConfig, mgmtConfig *nbconfig.Config, connectorConfi
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
srv := &migrationServer{store: migStore, eventStore: migEventStore}
|
||||
|
||||
pending, err := previewUsers(ctx, migStore)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -243,11 +265,14 @@ func migrateDB(cfg *migrationConfig, mgmtConfig *nbconfig.Config, connectorConfi
|
||||
}
|
||||
}
|
||||
|
||||
srv := &migrationServer{store: migStore, eventStore: migEventStore}
|
||||
if err := migration.MigrateUsersToStaticConnectors(srv, connectorConfig); err != nil {
|
||||
return fmt.Errorf("migrate users: %w", err)
|
||||
}
|
||||
|
||||
if err := migration.EnsureSingleAccountDomain(srv, cfg.singleAccountDomain); err != nil {
|
||||
return fmt.Errorf("prepare single account mode: %w", err)
|
||||
}
|
||||
|
||||
if !cfg.dryRun {
|
||||
log.Info("DB migration completed successfully")
|
||||
}
|
||||
|
||||
@@ -485,3 +485,77 @@ func TestGenerateConfig(t *testing.T) {
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateConfigRejectsUnusableSingleAccountDomain(t *testing.T) {
|
||||
base := func() migrationConfig {
|
||||
return migrationConfig{
|
||||
configPath: "/tmp/management.json",
|
||||
dataDir: "/tmp/datadir",
|
||||
idpSeedInfo: "seed",
|
||||
apiURL: "https://api.example.com",
|
||||
dashboardURL: "https://app.example.com",
|
||||
singleAccountDomain: migration.DefaultSingleAccountDomain,
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("usable domain is accepted", func(t *testing.T) {
|
||||
cfg := base()
|
||||
require.NoError(t, validateConfig(&cfg))
|
||||
})
|
||||
|
||||
t.Run("empty falls back to the default", func(t *testing.T) {
|
||||
cfg := base()
|
||||
cfg.singleAccountDomain = ""
|
||||
require.NoError(t, validateConfig(&cfg))
|
||||
})
|
||||
|
||||
// Rejected up front so the migration cannot fail after it has rewritten user IDs.
|
||||
t.Run("single label domain is rejected", func(t *testing.T) {
|
||||
cfg := base()
|
||||
cfg.singleAccountDomain = "corp"
|
||||
err := validateConfig(&cfg)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, migration.ErrUnusableDomain)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplyOverrides_SingleAccountDomainFromEnv(t *testing.T) {
|
||||
t.Run("env var overrides the flag", func(t *testing.T) {
|
||||
t.Setenv("NETBIRD_SINGLE_ACCOUNT_MODE_DOMAIN", "corp.example.com")
|
||||
|
||||
cfg, err := configFromArgs([]string{
|
||||
"--config", "/tmp/management.json",
|
||||
"--datadir", "/tmp/datadir",
|
||||
"--idp-seed-info", "seed",
|
||||
"--domain", "example.com",
|
||||
"--single-account-mode-domain", "flag.example.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "corp.example.com", cfg.singleAccountDomain)
|
||||
})
|
||||
|
||||
t.Run("unset leaves the flag value", func(t *testing.T) {
|
||||
cfg, err := configFromArgs([]string{
|
||||
"--config", "/tmp/management.json",
|
||||
"--datadir", "/tmp/datadir",
|
||||
"--idp-seed-info", "seed",
|
||||
"--domain", "example.com",
|
||||
"--single-account-mode-domain", "flag.example.com",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "flag.example.com", cfg.singleAccountDomain)
|
||||
})
|
||||
|
||||
t.Run("unusable env value is rejected", func(t *testing.T) {
|
||||
t.Setenv("NETBIRD_SINGLE_ACCOUNT_MODE_DOMAIN", "corp")
|
||||
|
||||
_, err := configFromArgs([]string{
|
||||
"--config", "/tmp/management.json",
|
||||
"--datadir", "/tmp/datadir",
|
||||
"--idp-seed-info", "seed",
|
||||
"--domain", "example.com",
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, migration.ErrUnusableDomain)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user