Skip to content

Refactor own profile management in the Go core#147

Open
pappz wants to merge 9 commits into
mainfrom
refactor/migrate-profiles-to-go
Open

Refactor own profile management in the Go core#147
pappz wants to merge 9 commits into
mainfrom
refactor/migrate-profiles-to-go

Conversation

@pappz

@pappz pappz commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Replace the parallel Swift profile manager with a thin wrapper over the core's ID-based profilemanager.ServiceManager (via the new NetBirdSDK iOS binding), eliminating the duplicate implementation. Profiles are now ID-keyed (on-disk filename = id, display name lives in the config); the default profile stays "default" at the container root and never becomes a hex id, matching the Go/desktop semantics.

  • ProfileManager: thin wrapper over NetBirdSDKProfileManager on iOS; tvOS keeps its single-default, container-root behavior (no Go profile manager)
  • ProfileLayoutMigration: one-time, idempotent, file-coordinated migration from the legacy profiles// directory layout to the Go layout; preserves auth tokens (copies unparseable configs verbatim), keeps the active selection and logged-out profiles, and only marks itself done on full success so a failure retries on the next launch
  • ProfileConnectionCache and all callers re-keyed by profile id
  • Settings/server changes already target the active profile through the profile manager (Preferences.configFile -> activeConfigPath)
  • Tests: NetBirdTests/ProfileMigrationTests covers the migration, reads it back through the Go lib, and round-trips a setting via NetBirdSDKPreferences (the test target now links NetBirdSDK)
  • Bump netbird-core submodule to include the iOS profile manager binding

Description

Summary by CodeRabbit

  • Refactor

    • Updated profile handling across the app to use stable profile IDs for loading, switching, and displaying connection details.
    • Updated “Current” management server URL and profile list actions to follow the active profile by ID.
    • Added a one-time, automatic migration to convert existing profiles to the new ID-based layout.
  • Tests

    • Added iOS-only profile migration tests covering idempotency, default/non-default behavior, and handling of deleted/logged-out profiles.
  • Chores

    • Skip Firebase setup during unit tests; enabled test workflow triggers for push and pull requests.

Replace the parallel Swift profile manager with a thin wrapper over the
core's ID-based profilemanager.ServiceManager (via the new NetBirdSDK iOS
binding), eliminating the duplicate implementation. Profiles are now
ID-keyed (on-disk filename = id, display name lives in the config); the
default profile stays "default" at the container root and never becomes a
hex id, matching the Go/desktop semantics.

- ProfileManager: thin wrapper over NetBirdSDKProfileManager on iOS; tvOS
  keeps its single-default, container-root behavior (no Go profile manager)
- ProfileLayoutMigration: one-time, idempotent, file-coordinated migration
  from the legacy profiles/<name>/ directory layout to the Go layout;
  preserves auth tokens (copies unparseable configs verbatim), keeps the
  active selection and logged-out profiles, and only marks itself done on
  full success so a failure retries on the next launch
- ProfileConnectionCache and all callers re-keyed by profile id
- Settings/server changes already target the active profile through the
  profile manager (Preferences.configFile -> activeConfigPath)
- Tests: NetBirdTests/ProfileMigrationTests covers the migration, reads it
  back through the Go lib, and round-trips a setting via NetBirdSDKPreferences
  (the test target now links NetBirdSDK)
- Bump netbird-core submodule to include the iOS profile manager binding
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5bb56463-1512-4f08-b3d7-c51a3ee4e8ba

📥 Commits

Reviewing files that changed from the base of the PR and between c4c7bba and 1edabae.

📒 Files selected for processing (3)
  • NetBird.xcodeproj/project.pbxproj
  • NetBirdTests/ProfileMigrationTests.swift
  • netbird-core
🚧 Files skipped from review as they are similar to previous changes (2)
  • NetBirdTests/ProfileMigrationTests.swift
  • NetBird.xcodeproj/project.pbxproj

📝 Walkthrough

Walkthrough

The PR replaces the native Swift name-based multi-profile filesystem implementation with a Go SDK (NetBirdSDKProfileManager) backed wrapper. It adds a one-time idempotent ProfileLayoutMigration that converts legacy per-name directories to flat ID-keyed files, renames ProfileConnectionCache APIs to use forID, updates all call sites in ViewModels/Views/NetworkExtensionAdapter, guards Firebase configuration from unit tests, and wires new files into Xcode build phases with a submodule bump.

Changes

Profile ID Migration to Go SDK

Layer / File(s) Summary
Profile model, Go SDK wrapper, and manager APIs
NetbirdKit/ProfileManager.swift
Profile is redefined with id, name, isActive, and isDefault computed property. ProfileManager becomes an iOS-only wrapper over NetBirdSDKProfileManager: initializes via containerBasePath/ProfileLayoutMigration, delegates listing/mutations to Go, adds getActiveProfileID, renames path accessors to forID, and changes managementURL resolution to parse config JSON first via readManagementURL.
ProfileLayoutMigration: one-time filesystem conversion
NetbirdKit/ProfileLayoutMigration.swift
New file implementing complete one-time migration: runIfNeeded uses NSFileCoordinator for cross-process serialization; performMigration detects legacy layout and calls migrateOne per profile directory; migrateOne converts per-name directories to flat ID-keyed files, preserves JSON fields, stamps Name, moves state files, and seeds ProfileConnectionCache; finalizes with writeActiveProfile, cleanupLegacy, and marker file write only on full success.
ProfileConnectionCache API rename to forID
NetbirdKit/ProfileConnectionCache.swift
All instance method external parameter labels renamed from for profile to forID id across entry, managementURL, save, saveManagementURL, clearConnectionData, and remove.
NetworkExtensionAdapter switches to ID-based profile lookup
NetbirdKit/NetworkExtensionAdapter.swift
restoreConfigIfMissing and the login IPC message builder both switch from name-based profile/cache lookups to getActiveProfileID/managementURL(forID:).
ViewModels and Views updated to ID-based APIs
NetBird/Source/App/ViewModels/AddProfileViewModel.swift, NetBird/Source/App/ViewModels/MainViewModel.swift, NetBird/Source/App/Views/ServerView.swift, NetBird/Source/App/Views/iOS/ProfilesListView.swift
AddProfileViewModel uses the returned Profile.id for config path and rollback. MainViewModel updates init, polling cache writes, and switchConnectionInfo to use forID-based APIs. ServerView and ProfilesListView replace all for: name calls with forID: id and use profile.isDefault instead of name string comparison.
ProfileMigrationTests: migration correctness and idempotency
NetBirdTests/ProfileMigrationTests.swift
New iOS-only XCTest suite with per-test temp dirs covering default profile stays at root, named profile migration to profiles/<id>.json, logged-out profile recovery from sidecar URL, tombstoned profiles excluded, settings round-trip via NetBirdSDKNewPreferences, idempotent re-run, and fresh-install marker write. Includes helpers for legacy layout construction and Go profile manager interfacing.
Firebase configuration and unit test guards
NetBird/Source/App/NetBirdApp.swift
Adds unit-test detection via environment variable and centralizes Firebase configuration behind configureFirebaseIfNeeded helper that skips configuration under XCTest and otherwise loads GoogleService-Info.plist into FirebaseOptions. Updates AppDelegate and tvOS NetBirdApp init to call the new helper.
Xcode project wiring and netbird-core submodule
NetBird.xcodeproj/project.pbxproj, .github/workflows/test.yml, netbird-core
Adds NetBirdSDK.xcframework to the frameworks build phase, wires ProfileLayoutMigration.swift into all target source phases, adds ProfileMigrationTests.swift to the test target, sets FRAMEWORK_SEARCH_PATHS and bumps IPHONEOS_DEPLOYMENT_TARGET to 15.0 for test configurations, enables GitHub Actions workflow triggers, corrects dummy GoogleService-Info.plist values, and bumps the netbird-core submodule pointer.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • netbirdio/ios-client#80: This PR directly updates the multi-profile code introduced in PR #80 across ProfileManager, AddProfileViewModel, MainViewModel, ServerView, and ProfilesListView to use ID-based APIs instead of name-based.

Suggested reviewers

  • mlsmaycon
  • doromaraujo

Poem

🐇 Hoppity-hop through the config files I go,
From names to IDs, a cleaner burrow to show!
The Go SDK calls, the migration runs true,
No profile left stranded—each one gets its due.
With markers and barriers, idempotent delight,
This rabbit's refactor lands perfectly right! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: refactoring profile management to use a Go core wrapper instead of the native Swift implementation.
Description check ✅ Passed The description comprehensively covers the refactoring scope, migration strategy, API changes, platform-specific behavior, testing approach, and submodule updates. It aligns with the template's minimal requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/migrate-profiles-to-go
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch refactor/migrate-profiles-to-go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Run the iOS test suite on PRs again to check whether the previously reported test crash still reproduces.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@NetBird.xcodeproj/project.pbxproj`:
- Line 1262: The FRAMEWORK_SEARCH_PATHS setting is configured with an overly
permissive pattern "$(PROJECT_DIR)/**" in two locations (near lines 1262 and
1796), which allows frameworks to be resolved from any subdirectory in the
project and can cause unexpected framework resolution. Replace both instances of
FRAMEWORK_SEARCH_PATHS = ("$(inherited)", "$(PROJECT_DIR)/**") with either a
specific explicit directory path that is actually needed, or remove the overly
broad path entirely and rely only on inherited settings and direct file
references to ensure only expected frameworks are resolved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fede5819-c5c6-42d7-b1c3-3fdbc2098642

📥 Commits

Reviewing files that changed from the base of the PR and between 8bcdb4f and 196a221.

📒 Files selected for processing (11)
  • NetBird.xcodeproj/project.pbxproj
  • NetBird/Source/App/ViewModels/AddProfileViewModel.swift
  • NetBird/Source/App/ViewModels/MainViewModel.swift
  • NetBird/Source/App/Views/ServerView.swift
  • NetBird/Source/App/Views/iOS/ProfilesListView.swift
  • NetBirdTests/ProfileMigrationTests.swift
  • NetbirdKit/NetworkExtensionAdapter.swift
  • NetbirdKit/ProfileConnectionCache.swift
  • NetbirdKit/ProfileLayoutMigration.swift
  • NetbirdKit/ProfileManager.swift
  • netbird-core

Comment thread NetBird.xcodeproj/project.pbxproj Outdated
pappz added 6 commits June 21, 2026 12:04
The non-null String path getters (getActiveConfigPath/getActiveStateFilePath/getConfigPath/getStateFilePath) are generated by gomobile as value-returning calls with an explicit NSErrorPointer parameter, not throwing methods. Wrap them via a goPath helper in ProfileManager (and matching helpers in the tests), fixing the iOS app build failure (missing argument for parameter).
The NetBirdTests target was iOS 14.0 while the NetBird app module requires iOS 15.0, so `@testable import NetBird` failed to build ("Compiling for iOS 14.0, but module 'NetBird' has a minimum deployment target of iOS 15.0"). Match the test target to the app so the test bundle compiles and the suite can run.
The dummy GoogleService-Info.plist used a GOOGLE_APP_ID of "dummy",
which fails Firebase's app-ID format validation. At launch the host app
calls FirebaseApp.configure(options:), and +[FIRApp addAppToAppDictionary:]
raised an uncaught NSException, aborting the host before the test runner
could establish a connection ("Test crashed with signal abrt before
establishing connection"). This blocked all hosted tests, not just the
new profile-migration ones.

Give the dummy app ID a well-formed value so configure() succeeds.
The host app calls FirebaseApp.configure() at launch. When launched purely
to host unit tests, this aborted the process before the test runner could
connect because FirebaseApp.configure() raises an uncaught ObjC exception on
an invalid app ID (the CI dummy plist).

Gate both the iOS and tvOS configure paths on the XCTestConfigurationFilePath
environment variable so Firebase is never initialized in the test host, and
fold the duplicated configure logic into a single helper.
The legacy netbird.cfg is written by the Go SDK, which serializes
Config.ManagementURL (a url.URL) as a nested {Scheme,Host,Path} object,
not a plain string. The settings round-trip test drives NetBirdSDKPreferences,
which fully unmarshals Config and rejected the string fixture with
"cannot unmarshal string into Go struct field Config.ManagementURL".
Normalize the fixture to the object form the SDK actually produces.
The test target used a recursive "$(PROJECT_DIR)/**" framework search path,
scanning every subdirectory (DerivedData, netbird-core, package checkouts) and
risking unexpected framework resolution. NetBirdSDK.xcframework lives at the
project root, so point the search path at "$(PROJECT_DIR)" directly.
@theodorsm

Copy link
Copy Markdown

/testflight

@github-actions

Copy link
Copy Markdown

TestFlight builds uploaded 0.2.1 (63) for 77a1b66 — iOS + tvOS

View workflow run

@theodorsm theodorsm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running a build with testflght I found two problems:

  1. Profile names are still restrictive (no special chars etc). We need to update isNameValid in AddProfileSheet, preferably using a binding for sanitizeDisplayName if possible.
  2. The profile listing/select/active are showing IDs as the profile names after adding a new profile with this PR:
image

I have tried to triage the reason for 2. but I can't seem to understand why it happens, the code looks fine to me. It is worth nothing that the name field in the json config is empty after creating a new profile.

@theodorsm

Copy link
Copy Markdown

Another thing we have to keep in mind with the go bindings vs. native swift implementation is that it will be harder to use native libraries such as the keychain (we don't use it now anyways) after this refactor. The profiles contains the wg and ssh private keys that are sensitive, we should consider extra protection beyond the regular app sandbox (keychain or at least exclude from backup).

@pappz

pappz commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Another thing we have to keep in mind with the go bindings vs. native swift implementation is that it will be harder to use native libraries such as the keychain (we don't use it now anyways) after this refactor. The profiles contains the wg and ssh private keys that are sensitive, we should consider extra protection beyond the regular app sandbox (keychain or at least exclude from backup).

Good point! I have to consider this.

@pappz

pappz commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Running a build with testflght I found two problems:

  1. Profile names are still restrictive (no special chars etc). We need to update isNameValid in AddProfileSheet, preferably using a binding for sanitizeDisplayName if possible.
  2. The profile listing/select/active are showing IDs as the profile names after adding a new profile with this PR:
image I have tried to triage the reason for 2. but I can't seem to understand why it happens, the code looks fine to me. It is worth nothing that the `name` field in the json config is empty after creating a new profile.

Please check this change: netbirdio/netbird@39d189b

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants