mirror of
https://github.com/netbirdio/netbird.git
synced 2025-06-25 04:01:29 +02:00
feat: add profile management commands for listing, adding, removing, and selecting profiles
This commit is contained in:
parent
790484bda2
commit
4b68a2a665
147
client/cmd/profile.go
Normal file
147
client/cmd/profile.go
Normal 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
|
||||||
|
}
|
@ -152,6 +152,7 @@ func init() {
|
|||||||
rootCmd.AddCommand(networksCMD)
|
rootCmd.AddCommand(networksCMD)
|
||||||
rootCmd.AddCommand(forwardingRulesCmd)
|
rootCmd.AddCommand(forwardingRulesCmd)
|
||||||
rootCmd.AddCommand(debugCmd)
|
rootCmd.AddCommand(debugCmd)
|
||||||
|
rootCmd.AddCommand(profileCmd)
|
||||||
|
|
||||||
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
|
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
|
||||||
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer 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(forCmd)
|
||||||
debugCmd.AddCommand(persistenceCmd)
|
debugCmd.AddCommand(persistenceCmd)
|
||||||
|
|
||||||
|
// profile commands
|
||||||
|
profileCmd.AddCommand(profileListCmd)
|
||||||
|
profileCmd.AddCommand(profileAddCmd)
|
||||||
|
profileCmd.AddCommand(profileRemoveCmd)
|
||||||
|
profileCmd.AddCommand(profileSelectCmd)
|
||||||
|
|
||||||
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
|
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
|
||||||
`Sets external IPs maps between local addresses and interfaces.`+
|
`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. `+
|
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
|
||||||
|
@ -4,13 +4,20 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultProfileName = "default"
|
||||||
|
)
|
||||||
|
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
@ -19,7 +26,6 @@ type Profile struct {
|
|||||||
|
|
||||||
type ProfileManager struct {
|
type ProfileManager struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
activeProfile *Profile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProfileManager() *ProfileManager {
|
func NewProfileManager() *ProfileManager {
|
||||||
@ -67,7 +73,7 @@ func (pm *ProfileManager) RemoveProfile(profileName string) error {
|
|||||||
return fmt.Errorf("failed to get active profile: %w", err)
|
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)
|
return fmt.Errorf("cannot remove active profile: %s", profileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,18 +88,8 @@ func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
|
|||||||
pm.mu.Lock()
|
pm.mu.Lock()
|
||||||
defer pm.mu.Unlock()
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
if pm.activeProfile == nil {
|
prof := pm.getActiveProfileState()
|
||||||
return nil, ErrNoActiveProfile
|
return &Profile{Name: prof}, nil
|
||||||
}
|
|
||||||
|
|
||||||
return pm.activeProfile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pm *ProfileManager) SetActiveProfile(profileName string) {
|
|
||||||
pm.mu.Lock()
|
|
||||||
defer pm.mu.Unlock()
|
|
||||||
|
|
||||||
pm.activeProfile = &Profile{Name: profileName}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *ProfileManager) ListProfiles() ([]Profile, error) {
|
func (pm *ProfileManager) ListProfiles() ([]Profile, error) {
|
||||||
@ -115,7 +111,7 @@ func (pm *ProfileManager) ListProfiles() ([]Profile, error) {
|
|||||||
|
|
||||||
var profiles []Profile
|
var profiles []Profile
|
||||||
// add default profile always
|
// 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 {
|
for _, file := range files {
|
||||||
profileName := strings.TrimSuffix(filepath.Base(file), ".json")
|
profileName := strings.TrimSuffix(filepath.Base(file), ".json")
|
||||||
var isActive bool
|
var isActive bool
|
||||||
@ -128,24 +124,13 @@ func (pm *ProfileManager) ListProfiles() ([]Profile, error) {
|
|||||||
return profiles, nil
|
return profiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(hakan): implement
|
|
||||||
func (pm *ProfileManager) SwitchProfile(profileName string) error {
|
func (pm *ProfileManager) SwitchProfile(profileName string) error {
|
||||||
pm.mu.Lock()
|
if err := pm.setActiveProfileState(profileName); err != nil {
|
||||||
defer pm.mu.Unlock()
|
return fmt.Errorf("failed to switch profile: %w", err)
|
||||||
|
|
||||||
// Check if the profile exists
|
|
||||||
configDir, err := getConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get config directory: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
profPath := filepath.Join(configDir, profileName+".json")
|
// TODO(hakan): implement the logic to switch the profile in the application
|
||||||
if !fileExists(profPath) {
|
|
||||||
return ErrProfileNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the active profile
|
|
||||||
pm.activeProfile = &Profile{Name: profileName}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,3 +144,57 @@ func sanitazeUsername(username string) string {
|
|||||||
return r
|
return r
|
||||||
}, username)
|
}, 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
|
||||||
|
}
|
||||||
|
@ -287,6 +287,7 @@ func (p *profileMenu) refresh() {
|
|||||||
item.Remove()
|
item.Remove()
|
||||||
item.cancel()
|
item.cancel()
|
||||||
}
|
}
|
||||||
|
p.profileSubItems = make([]*subItem, 0, len(profiles))
|
||||||
|
|
||||||
if p.manageProfilesSubItem != nil {
|
if p.manageProfilesSubItem != nil {
|
||||||
// Remove the manage profiles item if it exists
|
// Remove the manage profiles item if it exists
|
||||||
@ -327,7 +328,7 @@ func (p *profileMenu) refresh() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Infof("Switched to profile '%s'", profile.Name)
|
log.Infof("Switched to profile '%s'", profile.Name)
|
||||||
p.profileMenuItem.SetTitle(profile.Name)
|
p.refresh()
|
||||||
// TODO(hakan): update email menu item if needed
|
// TODO(hakan): update email menu item if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,6 +352,7 @@ func (p *profileMenu) refresh() {
|
|||||||
}
|
}
|
||||||
// Handle manage profiles click
|
// Handle manage profiles click
|
||||||
p.eventHandler.runSelfCommand("profiles", "true")
|
p.eventHandler.runSelfCommand("profiles", "true")
|
||||||
|
p.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user