mirror of
https://github.com/netbirdio/netbird.git
synced 2025-08-16 10:08:12 +02:00
[client] Feat: Support Multiple Profiles (#3980)
[client] Feat: Support Multiple Profiles (#3980)
This commit is contained in:
359
client/internal/profilemanager/service.go
Normal file
359
client/internal/profilemanager/service.go
Normal file
@ -0,0 +1,359 @@
|
||||
package profilemanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
|
||||
var (
|
||||
oldDefaultConfigPathDir = ""
|
||||
oldDefaultConfigPath = ""
|
||||
|
||||
DefaultConfigPathDir = ""
|
||||
DefaultConfigPath = ""
|
||||
ActiveProfileStatePath = ""
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorOldDefaultConfigNotFound = errors.New("old default config not found")
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
DefaultConfigPathDir = "/var/lib/netbird/"
|
||||
oldDefaultConfigPathDir = "/etc/netbird/"
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
oldDefaultConfigPathDir = filepath.Join(os.Getenv("PROGRAMDATA"), "Netbird")
|
||||
DefaultConfigPathDir = oldDefaultConfigPathDir
|
||||
|
||||
case "freebsd":
|
||||
oldDefaultConfigPathDir = "/var/db/netbird/"
|
||||
DefaultConfigPathDir = oldDefaultConfigPathDir
|
||||
}
|
||||
|
||||
oldDefaultConfigPath = filepath.Join(oldDefaultConfigPathDir, "config.json")
|
||||
DefaultConfigPath = filepath.Join(DefaultConfigPathDir, "default.json")
|
||||
ActiveProfileStatePath = filepath.Join(DefaultConfigPathDir, "active_profile.json")
|
||||
}
|
||||
|
||||
type ActiveProfileState struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func (a *ActiveProfileState) FilePath() (string, error) {
|
||||
if a.Name == "" {
|
||||
return "", fmt.Errorf("active profile name is empty")
|
||||
}
|
||||
|
||||
if a.Name == defaultProfileName {
|
||||
return DefaultConfigPath, nil
|
||||
}
|
||||
|
||||
configDir, err := getConfigDirForUser(a.Username)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get config directory for user %s: %w", a.Username, err)
|
||||
}
|
||||
|
||||
return filepath.Join(configDir, a.Name+".json"), nil
|
||||
}
|
||||
|
||||
type ServiceManager struct{}
|
||||
|
||||
func (s *ServiceManager) CopyDefaultProfileIfNotExists() (bool, error) {
|
||||
|
||||
if err := os.MkdirAll(DefaultConfigPathDir, 0600); err != nil {
|
||||
return false, fmt.Errorf("failed to create default config path directory: %w", err)
|
||||
}
|
||||
|
||||
// check if default profile exists
|
||||
if _, err := os.Stat(DefaultConfigPath); !os.IsNotExist(err) {
|
||||
// default profile already exists
|
||||
log.Debugf("default profile already exists at %s, skipping copy", DefaultConfigPath)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check old default profile
|
||||
if _, err := os.Stat(oldDefaultConfigPath); os.IsNotExist(err) {
|
||||
// old default profile does not exist, nothing to copy
|
||||
return false, ErrorOldDefaultConfigNotFound
|
||||
}
|
||||
|
||||
// copy old default profile to new location
|
||||
if err := copyFile(oldDefaultConfigPath, DefaultConfigPath, 0600); err != nil {
|
||||
return false, fmt.Errorf("copy default profile from %s to %s: %w", oldDefaultConfigPath, DefaultConfigPath, err)
|
||||
}
|
||||
|
||||
// set permissions for the new default profile
|
||||
if err := os.Chmod(DefaultConfigPath, 0600); err != nil {
|
||||
log.Warnf("failed to set permissions for default profile: %v", err)
|
||||
}
|
||||
|
||||
if err := s.SetActiveProfileState(&ActiveProfileState{
|
||||
Name: "default",
|
||||
Username: "",
|
||||
}); err != nil {
|
||||
log.Errorf("failed to set active profile state: %v", err)
|
||||
return false, fmt.Errorf("failed to set active profile state: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// copyFile copies the contents of src to dst and sets dst's file mode to perm.
|
||||
func copyFile(src, dst string, perm os.FileMode) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source file %s: %w", src, err)
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open target file %s: %w", dst, err)
|
||||
}
|
||||
defer func() {
|
||||
if cerr := out.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return fmt.Errorf("copy data to %s: %w", dst, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceManager) CreateDefaultProfile() error {
|
||||
_, err := UpdateOrCreateConfig(ConfigInput{
|
||||
ConfigPath: DefaultConfigPath,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create default profile: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("default profile created at %s", DefaultConfigPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceManager) GetActiveProfileState() (*ActiveProfileState, error) {
|
||||
if err := s.setDefaultActiveState(); err != nil {
|
||||
return nil, fmt.Errorf("failed to set default active profile state: %w", err)
|
||||
}
|
||||
var activeProfile ActiveProfileState
|
||||
if _, err := util.ReadJson(ActiveProfileStatePath, &activeProfile); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if err := s.SetActiveProfileStateToDefault(); err != nil {
|
||||
return nil, fmt.Errorf("failed to set active profile to default: %w", err)
|
||||
}
|
||||
return &ActiveProfileState{
|
||||
Name: "default",
|
||||
Username: "",
|
||||
}, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to read active profile state: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if activeProfile.Name == "" {
|
||||
if err := s.SetActiveProfileStateToDefault(); err != nil {
|
||||
return nil, fmt.Errorf("failed to set active profile to default: %w", err)
|
||||
}
|
||||
return &ActiveProfileState{
|
||||
Name: "default",
|
||||
Username: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &activeProfile, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *ServiceManager) setDefaultActiveState() error {
|
||||
_, err := os.Stat(ActiveProfileStatePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := s.SetActiveProfileStateToDefault(); err != nil {
|
||||
return fmt.Errorf("failed to set active profile to default: %w", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("failed to stat active profile state path %s: %w", ActiveProfileStatePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceManager) SetActiveProfileState(a *ActiveProfileState) error {
|
||||
if a == nil || a.Name == "" {
|
||||
return errors.New("invalid active profile state")
|
||||
}
|
||||
|
||||
if a.Name != defaultProfileName && a.Username == "" {
|
||||
return fmt.Errorf("username must be set for non-default profiles, got: %s", a.Name)
|
||||
}
|
||||
|
||||
if err := util.WriteJsonWithRestrictedPermission(context.Background(), ActiveProfileStatePath, a); err != nil {
|
||||
return fmt.Errorf("failed to write active profile state: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("active profile set to %s for %s", a.Name, a.Username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceManager) SetActiveProfileStateToDefault() error {
|
||||
return s.SetActiveProfileState(&ActiveProfileState{
|
||||
Name: "default",
|
||||
Username: "",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ServiceManager) DefaultProfilePath() string {
|
||||
return DefaultConfigPath
|
||||
}
|
||||
|
||||
func (s *ServiceManager) AddProfile(profileName, username string) error {
|
||||
configDir, err := getConfigDirForUser(username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config directory: %w", err)
|
||||
}
|
||||
|
||||
profileName = sanitizeProfileName(profileName)
|
||||
|
||||
if profileName == defaultProfileName {
|
||||
return fmt.Errorf("cannot create profile with reserved name: %s", defaultProfileName)
|
||||
}
|
||||
|
||||
profPath := filepath.Join(configDir, profileName+".json")
|
||||
if fileExists(profPath) {
|
||||
return ErrProfileAlreadyExists
|
||||
}
|
||||
|
||||
cfg, err := createNewConfig(ConfigInput{ConfigPath: profPath})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new config: %w", err)
|
||||
}
|
||||
|
||||
err = util.WriteJson(context.Background(), profPath, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write profile config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceManager) RemoveProfile(profileName, username string) error {
|
||||
configDir, err := getConfigDirForUser(username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get config directory: %w", err)
|
||||
}
|
||||
|
||||
profileName = sanitizeProfileName(profileName)
|
||||
|
||||
if profileName == defaultProfileName {
|
||||
return fmt.Errorf("cannot remove profile with reserved name: %s", defaultProfileName)
|
||||
}
|
||||
profPath := filepath.Join(configDir, profileName+".json")
|
||||
if !fileExists(profPath) {
|
||||
return ErrProfileNotFound
|
||||
}
|
||||
|
||||
activeProf, err := s.GetActiveProfileState()
|
||||
if err != nil && !errors.Is(err, ErrNoActiveProfile) {
|
||||
return fmt.Errorf("failed to get active profile: %w", err)
|
||||
}
|
||||
|
||||
if activeProf != nil && activeProf.Name == profileName {
|
||||
return fmt.Errorf("cannot remove active profile: %s", profileName)
|
||||
}
|
||||
|
||||
err = util.RemoveJson(profPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove profile config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServiceManager) ListProfiles(username string) ([]Profile, error) {
|
||||
configDir, err := getConfigDirForUser(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config directory: %w", err)
|
||||
}
|
||||
|
||||
files, err := util.ListFiles(configDir, "*.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list profile files: %w", err)
|
||||
}
|
||||
|
||||
var filtered []string
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file, "state.json") {
|
||||
continue // skip state files
|
||||
}
|
||||
filtered = append(filtered, file)
|
||||
}
|
||||
sort.Strings(filtered)
|
||||
|
||||
var activeProfName string
|
||||
activeProf, err := s.GetActiveProfileState()
|
||||
if err == nil {
|
||||
activeProfName = activeProf.Name
|
||||
}
|
||||
|
||||
var profiles []Profile
|
||||
// add default profile always
|
||||
profiles = append(profiles, Profile{Name: defaultProfileName, IsActive: activeProfName == "" || activeProfName == defaultProfileName})
|
||||
for _, file := range filtered {
|
||||
profileName := strings.TrimSuffix(filepath.Base(file), ".json")
|
||||
var isActive bool
|
||||
if activeProfName != "" && activeProfName == profileName {
|
||||
isActive = true
|
||||
}
|
||||
profiles = append(profiles, Profile{Name: profileName, IsActive: isActive})
|
||||
}
|
||||
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// GetStatePath returns the path to the state file based on the operating system
|
||||
// It returns an empty string if the path cannot be determined.
|
||||
func (s *ServiceManager) GetStatePath() string {
|
||||
if path := os.Getenv("NB_DNS_STATE_FILE"); path != "" {
|
||||
return path
|
||||
}
|
||||
|
||||
defaultStatePath := filepath.Join(DefaultConfigPathDir, "state.json")
|
||||
|
||||
activeProf, err := s.GetActiveProfileState()
|
||||
if err != nil {
|
||||
log.Warnf("failed to get active profile state: %v", err)
|
||||
return defaultStatePath
|
||||
}
|
||||
|
||||
if activeProf.Name == defaultProfileName {
|
||||
return defaultStatePath
|
||||
}
|
||||
|
||||
configDir, err := getConfigDirForUser(activeProf.Username)
|
||||
if err != nil {
|
||||
log.Warnf("failed to get config directory for user %s: %v", activeProf.Username, err)
|
||||
return defaultStatePath
|
||||
}
|
||||
|
||||
return filepath.Join(configDir, activeProf.Name+".state.json")
|
||||
}
|
Reference in New Issue
Block a user