feat: add profile management commands for listing, adding, removing, and selecting profiles

This commit is contained in:
Hakan Sariman 2025-06-23 15:51:38 +03:00
parent 790484bda2
commit 4b68a2a665
4 changed files with 226 additions and 31 deletions

147
client/cmd/profile.go Normal file
View File

@ -0,0 +1,147 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/util"
)
var profileCmd = &cobra.Command{
Use: "profile",
Short: "manage Netbird profiles",
Long: `Manage Netbird profiles, allowing you to list, switch, and remove profiles.`,
}
var profileListCmd = &cobra.Command{
Use: "list",
Short: "list all profiles",
Long: `List all available profiles in the Netbird client.`,
RunE: listProfilesFunc,
}
var profileAddCmd = &cobra.Command{
Use: "add <profile_name>",
Short: "add a new profile",
Long: `Add a new profile to the Netbird client. The profile name must be unique.`,
Args: cobra.ExactArgs(1),
RunE: addProfileFunc,
}
var profileRemoveCmd = &cobra.Command{
Use: "remove <profile_name>",
Short: "remove a profile",
Long: `Remove a profile from the Netbird client. The profile must not be active.`,
Args: cobra.ExactArgs(1),
RunE: removeProfileFunc,
}
var profileSelectCmd = &cobra.Command{
Use: "select <profile_name>",
Short: "select a profile",
Long: `Select a profile to be the active profile in the Netbird client. The profile must exist.`,
Args: cobra.ExactArgs(1),
RunE: selectProfileFunc,
}
func listProfilesFunc(cmd *cobra.Command, _ []string) error {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(cmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return err
}
profileManager := profilemanager.NewProfileManager()
profiles, err := profileManager.ListProfiles()
if err != nil {
return err
}
// list profiles, add a tick if the profile is active
cmd.Println("Found", len(profiles), "profiles:")
for _, profile := range profiles {
// use a cross to indicate the passive profiles
activeMarker := "✗"
if profile.IsActive {
activeMarker = "✓"
}
cmd.Println(activeMarker, profile.Name)
}
return nil
}
func addProfileFunc(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(cmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return err
}
profileManager := profilemanager.NewProfileManager()
profileName := args[0]
err = profileManager.AddProfile(profilemanager.Profile{
Name: profileName,
})
if err != nil {
return err
}
cmd.Println("Profile added successfully:", profileName)
return nil
}
func removeProfileFunc(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(cmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return err
}
profileManager := profilemanager.NewProfileManager()
profileName := args[0]
err = profileManager.RemoveProfile(profileName)
if err != nil {
return err
}
cmd.Println("Profile removed successfully:", profileName)
return nil
}
func selectProfileFunc(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(cmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return err
}
profileManager := profilemanager.NewProfileManager()
profileName := args[0]
err = profileManager.SwitchProfile(profileName)
if err != nil {
return err
}
cmd.Println("Profile switched successfully to:", profileName)
return nil
}

View File

@ -152,6 +152,7 @@ func init() {
rootCmd.AddCommand(networksCMD)
rootCmd.AddCommand(forwardingRulesCmd)
rootCmd.AddCommand(debugCmd)
rootCmd.AddCommand(profileCmd)
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
@ -167,6 +168,12 @@ func init() {
debugCmd.AddCommand(forCmd)
debugCmd.AddCommand(persistenceCmd)
// profile commands
profileCmd.AddCommand(profileListCmd)
profileCmd.AddCommand(profileAddCmd)
profileCmd.AddCommand(profileRemoveCmd)
profileCmd.AddCommand(profileSelectCmd)
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
`Sets external IPs maps between local addresses and interfaces.`+
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+

View File

@ -4,13 +4,20 @@ import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/util"
)
const (
defaultProfileName = "default"
)
type Profile struct {
Name string
Email string
@ -18,8 +25,7 @@ type Profile struct {
}
type ProfileManager struct {
mu sync.Mutex
activeProfile *Profile
mu sync.Mutex
}
func NewProfileManager() *ProfileManager {
@ -67,7 +73,7 @@ func (pm *ProfileManager) RemoveProfile(profileName string) error {
return fmt.Errorf("failed to get active profile: %w", err)
}
if activeProf.Name == profileName {
if activeProf != nil && activeProf.Name == profileName {
return fmt.Errorf("cannot remove active profile: %s", profileName)
}
@ -82,18 +88,8 @@ func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
if pm.activeProfile == nil {
return nil, ErrNoActiveProfile
}
return pm.activeProfile, nil
}
func (pm *ProfileManager) SetActiveProfile(profileName string) {
pm.mu.Lock()
defer pm.mu.Unlock()
pm.activeProfile = &Profile{Name: profileName}
prof := pm.getActiveProfileState()
return &Profile{Name: prof}, nil
}
func (pm *ProfileManager) ListProfiles() ([]Profile, error) {
@ -115,7 +111,7 @@ func (pm *ProfileManager) ListProfiles() ([]Profile, error) {
var profiles []Profile
// add default profile always
profiles = append(profiles, Profile{Name: "default", IsActive: activeProfName == "default"})
profiles = append(profiles, Profile{Name: "default", IsActive: activeProfName == "" || activeProfName == "default"})
for _, file := range files {
profileName := strings.TrimSuffix(filepath.Base(file), ".json")
var isActive bool
@ -128,24 +124,13 @@ func (pm *ProfileManager) ListProfiles() ([]Profile, error) {
return profiles, nil
}
// TODO(hakan): implement
func (pm *ProfileManager) SwitchProfile(profileName string) error {
pm.mu.Lock()
defer pm.mu.Unlock()
// Check if the profile exists
configDir, err := getConfigDir()
if err != nil {
return fmt.Errorf("failed to get config directory: %w", err)
if err := pm.setActiveProfileState(profileName); err != nil {
return fmt.Errorf("failed to switch profile: %w", err)
}
profPath := filepath.Join(configDir, profileName+".json")
if !fileExists(profPath) {
return ErrProfileNotFound
}
// TODO(hakan): implement the logic to switch the profile in the application
// Set the active profile
pm.activeProfile = &Profile{Name: profileName}
return nil
}
@ -159,3 +144,57 @@ func sanitazeUsername(username string) string {
return r
}, username)
}
func (pm *ProfileManager) getActiveProfileState() string {
configDir, err := getConfigDir()
if err != nil {
log.Warnf("failed to get config directory: %v", err)
return defaultProfileName
}
statePath := filepath.Join(configDir, "active_profile.txt")
prof, err := os.ReadFile(statePath)
if err != nil {
if !os.IsNotExist(err) {
log.Warnf("failed to read active profile state: %v", err)
} else {
pm.setActiveProfileState(defaultProfileName)
}
return defaultProfileName
}
profileName := strings.TrimSpace(string(prof))
if profileName == "" {
log.Warnf("active profile state is empty, using default profile: %s", defaultProfileName)
return defaultProfileName
}
if !fileExists(filepath.Join(configDir, profileName+".json")) {
log.Warnf("active profile %s does not exist, using default profile: %s", profileName, defaultProfileName)
return defaultProfileName
}
return profileName
}
func (pm *ProfileManager) setActiveProfileState(profileName string) error {
configDir, err := getConfigDir()
if err != nil {
return fmt.Errorf("failed to get config directory: %w", err)
}
profPath := filepath.Join(configDir, profileName+".json")
if !fileExists(profPath) {
return fmt.Errorf("profile %s does not exist", profileName)
}
statePath := filepath.Join(configDir, "active_profile.txt")
err = os.WriteFile(statePath, []byte(profileName), 0644)
if err != nil {
return fmt.Errorf("failed to write active profile state: %w", err)
}
return nil
}

View File

@ -287,6 +287,7 @@ func (p *profileMenu) refresh() {
item.Remove()
item.cancel()
}
p.profileSubItems = make([]*subItem, 0, len(profiles))
if p.manageProfilesSubItem != nil {
// Remove the manage profiles item if it exists
@ -327,7 +328,7 @@ func (p *profileMenu) refresh() {
return
}
log.Infof("Switched to profile '%s'", profile.Name)
p.profileMenuItem.SetTitle(profile.Name)
p.refresh()
// TODO(hakan): update email menu item if needed
}
}
@ -351,6 +352,7 @@ func (p *profileMenu) refresh() {
}
// Handle manage profiles click
p.eventHandler.runSelfCommand("profiles", "true")
p.refresh()
}
}
}()