mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-23 08:51:29 +02:00
## Summary Adds a unified `admin` CLI for self-hosted instance administrators in both the management and combined binaries. ## User Management ### `admin user change-password` - Changes a local embedded IdP user's password. - Selects the user with `--email` or `--user-id`. - Reads the new password from `--password` or `--password-file`. - Clears the user's local authentication session so the new password is required on the next login. - **Alias:** `admin user set-password`. ### `admin user reset-mfa` - Resets a local embedded IdP user's MFA enrollment. - Selects the user with `--email` or `--user-id`. - Clears TOTP/WebAuthn enrollment data and removes the local authentication session. - The user will re-enroll MFA on the next login. ## MFA Management ### `admin mfa status` - Shows whether local MFA is enabled in the account settings. - Checks the embedded IdP client configuration and reports whether MFA is enabled there. ### `admin mfa enable` - Enables local MFA for embedded IdP users. - Updates embedded IdP clients and saves the account MFA setting. - Records an audit event on a best-effort basis. ### `admin mfa disable` - Disables local MFA for embedded IdP users. - Updates embedded IdP clients and saves the account MFA setting. - Records an audit event on a best-effort basis. ## Reverse Proxy Tokens ### `admin token create --name <name> [--expires-in <duration>]` - Creates a reverse proxy access token. - Prints the plaintext token once, along with the token ID. - `--expires-in` supports values such as `24h`, `30d`, or `365d`. If omitted, the token never expires. ### `admin token list` - Lists reverse proxy access tokens. - Shows the token ID, name, creation date, expiration, last-used time, and revocation status. - **Alias:** `admin token ls`. ### `admin token revoke <token-id>` - Revokes a reverse proxy access token. - Revoked tokens can no longer authenticate reverse proxy instances. ## Reverse Proxy Management ### `admin proxy disconnect-all` - Lists registered reverse proxy instances and force-marks all connected instances as disconnected. - Useful for repairing stale proxy state after an unclean management server shutdown. - Prompts for confirmation by default. - `--dry-run` previews the changes without applying them. - `--force` skips the confirmation prompt. - Live proxies may appear again after their next heartbeat, reconnect, or re-registration. ## Compatibility Commands ### `token ...` - Deprecated top-level compatibility path. - Behaves the same as `admin token ...`. - Retained so existing scripts using `token create`, `token list`, or `token revoke` continue to work. ## Changes - Adds reusable `management/cmd/admin` command package. - Wires `admin` into `netbird-mgmt` and `combined`. - Adds local user password reset with existing password strength validation. - Adds local MFA enrollment reset by clearing Dex TOTP/WebAuthn credentials and local auth sessions. - Adds local MFA enable/disable/status helpers for embedded IdP deployments. - Moves proxy access token commands under `admin token` for a single admin-focused CLI entry point. - Exports `server.ValidatePassword` for reuse by CLI helpers. ## Tests ```bash go test ./management/cmd/... go test ./management/cmd/admin ./management/cmd ./combined/cmd go test ./management/server -run TestValidatePassword ``` Pre-push lint also passed. ### Checklist - [ ] Is it a bug fix - [ ] Is a typo/documentation fix - [x] Is a feature enhancement - [ ] It is a refactor - [ ] Created tests that fail without the change (if possible) - [ ] This change does **not** modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — **OR** I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See [CONTRIBUTING.md](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTING.md#discuss-changes-with-the-netbird-team-first). > By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md). ## Documentation Select exactly one: - [x] I added/updated documentation for this change - [ ] Documentation is **not needed** for this change (explain why) ### Docs PR URL (required if "docs added" is checked) Paste the PR link from https://github.com/netbirdio/docs here: https://github.com/netbirdio/docs/pull/832 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added self-hosted admin CLI commands for changing passwords, resetting MFA (including WebAuthn), and managing embedded IdP client MFA (enable/disable/status). * Introduced a unified admin command entry point and improved data-directory handling for embedded IdP storage. * **Refactor** * Centralized password strength validation into a shared exported validator. * **Tests** * Added a comprehensive admin command test suite covering password input, selectors, MFA reset, and client MFA state handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
521 lines
16 KiB
Go
521 lines
16 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
|
|
"github.com/netbirdio/netbird/formatter/hook"
|
|
"github.com/netbirdio/netbird/management/internals/server"
|
|
nbconfig "github.com/netbirdio/netbird/management/internals/server/config"
|
|
nbdomain "github.com/netbirdio/netbird/shared/management/domain"
|
|
"github.com/netbirdio/netbird/shared/management/grpc"
|
|
"github.com/netbirdio/netbird/util"
|
|
"github.com/netbirdio/netbird/util/crypt"
|
|
)
|
|
|
|
var newServer = func(cfg *server.Config) server.Server {
|
|
return server.NewServer(cfg)
|
|
}
|
|
|
|
func SetNewServer(fn func(*server.Config) server.Server) {
|
|
newServer = fn
|
|
}
|
|
|
|
var (
|
|
config *nbconfig.Config
|
|
|
|
mgmtCmd = &cobra.Command{
|
|
Use: "management",
|
|
Short: "start NetBird Management Server",
|
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
flag.Parse()
|
|
|
|
//nolint
|
|
ctx := context.WithValue(cmd.Context(), hook.ExecutionContextKey, hook.SystemSource)
|
|
|
|
err := util.InitLog(logLevel, logFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed initializing log %v", err)
|
|
}
|
|
|
|
// detect whether user specified a port
|
|
userPort := cmd.Flag("port").Changed
|
|
|
|
config, err = LoadMgmtConfig(ctx, nbconfig.MgmtConfigPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed reading provided config file: %s: %v", nbconfig.MgmtConfigPath, err)
|
|
}
|
|
|
|
if cmd.Flag(idpSignKeyRefreshEnabledFlagName).Changed {
|
|
config.HttpConfig.IdpSignKeyRefreshEnabled = idpSignKeyRefreshEnabled
|
|
}
|
|
|
|
var tlsEnabled bool
|
|
if mgmtLetsencryptDomain != "" || (config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "") {
|
|
tlsEnabled = true
|
|
}
|
|
|
|
if !userPort {
|
|
// different defaults for port when tls enabled/disabled
|
|
if tlsEnabled {
|
|
mgmtPort = 443
|
|
} else {
|
|
mgmtPort = 80
|
|
}
|
|
}
|
|
|
|
if !nbdomain.IsValidDomainNoWildcard(dnsDomain) {
|
|
return fmt.Errorf("invalid dns-domain: %s", dnsDomain)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
flag.Parse()
|
|
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
defer cancel()
|
|
//nolint
|
|
ctx = context.WithValue(ctx, hook.ExecutionContextKey, hook.SystemSource)
|
|
|
|
err := handleRebrand(cmd)
|
|
if err != nil {
|
|
return fmt.Errorf("migrate files %v", err)
|
|
}
|
|
|
|
if _, err = os.Stat(config.Datadir); os.IsNotExist(err) {
|
|
err = os.MkdirAll(config.Datadir, 0755)
|
|
if err != nil {
|
|
return fmt.Errorf("failed creating datadir: %s: %v", config.Datadir, err)
|
|
}
|
|
}
|
|
|
|
if disableSingleAccMode {
|
|
mgmtSingleAccModeDomain = ""
|
|
}
|
|
|
|
srv := newServer(&server.Config{
|
|
NbConfig: config,
|
|
DNSDomain: dnsDomain,
|
|
MgmtSingleAccModeDomain: mgmtSingleAccModeDomain,
|
|
MgmtPort: mgmtPort,
|
|
MgmtMetricsPort: mgmtMetricsPort,
|
|
DisableLegacyManagementPort: disableLegacyManagementPort,
|
|
DisableMetrics: disableMetrics,
|
|
DisableGeoliteUpdate: disableGeoliteUpdate,
|
|
UserDeleteFromIDPEnabled: userDeleteFromIDPEnabled,
|
|
})
|
|
go func() {
|
|
if err := srv.Start(cmd.Context()); err != nil {
|
|
log.Fatalf("Server error: %v", err)
|
|
}
|
|
}()
|
|
|
|
stopChan := make(chan os.Signal, 1)
|
|
signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM)
|
|
select {
|
|
case <-stopChan:
|
|
log.Info("Received shutdown signal, stopping server...")
|
|
err = srv.Stop()
|
|
if err != nil {
|
|
log.Errorf("Failed to stop server gracefully: %v", err)
|
|
}
|
|
case err := <-srv.Errors():
|
|
log.Fatalf("Server stopped unexpectedly: %v", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
)
|
|
|
|
func LoadMgmtConfig(ctx context.Context, mgmtConfigPath string) (*nbconfig.Config, error) {
|
|
loadedConfig := &nbconfig.Config{}
|
|
if _, err := util.ReadJsonWithEnvSub(mgmtConfigPath, loadedConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ApplyCommandLineOverrides(loadedConfig)
|
|
|
|
err := grpc.ValidateSyncMessageVersion(loadedConfig.HighestSupportedSyncMessageVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for account, version := range loadedConfig.PerAccountHighestSupportedSyncMessageVersion {
|
|
err := grpc.ValidateSyncMessageVersion(&version)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unrecognized sync message version for account %s, %w", account, err)
|
|
}
|
|
}
|
|
|
|
// Apply EmbeddedIdP config to HttpConfig if embedded IdP is enabled
|
|
err = ApplyEmbeddedIdPConfig(ctx, loadedConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := ApplyOIDCConfig(ctx, loadedConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
LogConfigInfo(loadedConfig)
|
|
|
|
if err := EnsureEncryptionKey(ctx, mgmtConfigPath, loadedConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return loadedConfig, nil
|
|
}
|
|
|
|
// ApplyCommandLineOverrides applies command-line flag overrides to the config
|
|
func ApplyCommandLineOverrides(cfg *nbconfig.Config) {
|
|
if mgmtLetsencryptDomain != "" {
|
|
cfg.HttpConfig.LetsEncryptDomain = mgmtLetsencryptDomain
|
|
}
|
|
if mgmtDataDir != "" {
|
|
cfg.Datadir = mgmtDataDir
|
|
}
|
|
if certKey != "" && certFile != "" {
|
|
cfg.HttpConfig.CertFile = certFile
|
|
cfg.HttpConfig.CertKey = certKey
|
|
}
|
|
}
|
|
|
|
// ApplyEmbeddedIdPConfig populates HttpConfig and EmbeddedIdP storage from config when embedded IdP is enabled.
|
|
// This allows users to only specify EmbeddedIdP config without duplicating values in HttpConfig.
|
|
func ApplyEmbeddedIdPConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
|
if cfg.EmbeddedIdP == nil || !cfg.EmbeddedIdP.Enabled {
|
|
return nil
|
|
}
|
|
|
|
// apply some defaults based on the EmbeddedIdP config
|
|
if disableSingleAccMode {
|
|
// Embedded IdP requires single account mode - multiple account mode is not supported
|
|
return fmt.Errorf("embedded IdP requires single account mode; multiple account mode is not supported with embedded IdP. Please remove --disable-single-account-mode flag")
|
|
}
|
|
// Enable user deletion from IDP by default if EmbeddedIdP is enabled
|
|
userDeleteFromIDPEnabled = true
|
|
|
|
// Set LocalAddress for embedded IdP if enabled, used for internal JWT validation
|
|
cfg.EmbeddedIdP.LocalAddress = fmt.Sprintf("localhost:%d", mgmtPort)
|
|
|
|
// Set storage defaults based on Datadir
|
|
if cfg.EmbeddedIdP.Storage.Type == "" {
|
|
cfg.EmbeddedIdP.Storage.Type = "sqlite3"
|
|
}
|
|
if cfg.EmbeddedIdP.Storage.Config.File == "" && cfg.Datadir != "" {
|
|
cfg.EmbeddedIdP.Storage.Config.File = filepath.Join(cfg.Datadir, "idp.db")
|
|
}
|
|
|
|
issuer := cfg.EmbeddedIdP.Issuer
|
|
|
|
if cfg.HttpConfig != nil {
|
|
log.WithContext(ctx).Warnf("overriding HttpConfig with EmbeddedIdP config. " +
|
|
"HttpConfig is ignored when EmbeddedIdP is enabled. Please remove HttpConfig section from the config file")
|
|
} else {
|
|
// Ensure HttpConfig exists. We need it for backwards compatibility with the old config format.
|
|
cfg.HttpConfig = &nbconfig.HttpServerConfig{}
|
|
}
|
|
|
|
// Set HttpConfig values from EmbeddedIdP
|
|
cfg.HttpConfig.AuthIssuer = issuer
|
|
cfg.HttpConfig.AuthAudience = "netbird-dashboard"
|
|
cfg.HttpConfig.AuthClientID = cfg.HttpConfig.AuthAudience
|
|
cfg.HttpConfig.CLIAuthAudience = "netbird-cli"
|
|
cfg.HttpConfig.AuthUserIDClaim = "sub"
|
|
cfg.HttpConfig.AuthKeysLocation = issuer + "/keys"
|
|
cfg.HttpConfig.OIDCConfigEndpoint = issuer + "/.well-known/openid-configuration"
|
|
cfg.HttpConfig.IdpSignKeyRefreshEnabled = true
|
|
callbackURL := strings.TrimSuffix(cfg.HttpConfig.AuthIssuer, "/oauth2")
|
|
cfg.HttpConfig.AuthCallbackURL = callbackURL + types.ProxyCallbackEndpointFull
|
|
|
|
return nil
|
|
}
|
|
|
|
// ApplyOIDCConfig fetches and applies OIDC configuration if endpoint is specified
|
|
func ApplyOIDCConfig(ctx context.Context, cfg *nbconfig.Config) error {
|
|
oidcEndpoint := cfg.HttpConfig.OIDCConfigEndpoint
|
|
if oidcEndpoint == "" {
|
|
return nil
|
|
}
|
|
|
|
if cfg.EmbeddedIdP != nil && cfg.EmbeddedIdP.Enabled {
|
|
// skip OIDC config fetching if EmbeddedIdP is enabled as it is unnecessary given it is embedded
|
|
return nil
|
|
}
|
|
|
|
log.WithContext(ctx).Infof("loading OIDC configuration from the provided IDP configuration endpoint %s", oidcEndpoint)
|
|
oidcConfig, err := fetchOIDCConfig(ctx, oidcEndpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.WithContext(ctx).Infof("loaded OIDC configuration from the provided IDP configuration endpoint: %s", oidcEndpoint)
|
|
|
|
log.WithContext(ctx).Infof("overriding HttpConfig.AuthIssuer with a new value %s, previously configured value: %s",
|
|
oidcConfig.Issuer, cfg.HttpConfig.AuthIssuer)
|
|
cfg.HttpConfig.AuthIssuer = oidcConfig.Issuer
|
|
|
|
log.WithContext(ctx).Infof("overriding HttpConfig.AuthKeysLocation (JWT certs) with a new value %s, previously configured value: %s",
|
|
oidcConfig.JwksURI, cfg.HttpConfig.AuthKeysLocation)
|
|
cfg.HttpConfig.AuthKeysLocation = oidcConfig.JwksURI
|
|
|
|
if err := ApplyDeviceAuthFlowConfig(ctx, cfg, &oidcConfig, oidcEndpoint); err != nil {
|
|
return err
|
|
}
|
|
ApplyPKCEFlowConfig(ctx, cfg, &oidcConfig)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ApplyDeviceAuthFlowConfig applies OIDC config to DeviceAuthorizationFlow if enabled
|
|
func ApplyDeviceAuthFlowConfig(ctx context.Context, cfg *nbconfig.Config, oidcConfig *OIDCConfigResponse, oidcEndpoint string) error {
|
|
if cfg.DeviceAuthorizationFlow == nil || strings.ToLower(cfg.DeviceAuthorizationFlow.Provider) == string(nbconfig.NONE) {
|
|
return nil
|
|
}
|
|
|
|
log.WithContext(ctx).Infof("overriding DeviceAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
|
oidcConfig.TokenEndpoint, cfg.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
|
cfg.DeviceAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
|
|
|
log.WithContext(ctx).Infof("overriding DeviceAuthorizationFlow.DeviceAuthEndpoint with a new value: %s, previously configured value: %s",
|
|
oidcConfig.DeviceAuthEndpoint, cfg.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint)
|
|
cfg.DeviceAuthorizationFlow.ProviderConfig.DeviceAuthEndpoint = oidcConfig.DeviceAuthEndpoint
|
|
|
|
u, err := url.Parse(oidcEndpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.WithContext(ctx).Infof("overriding DeviceAuthorizationFlow.ProviderConfig.Domain with a new value: %s, previously configured value: %s",
|
|
u.Host, cfg.DeviceAuthorizationFlow.ProviderConfig.Domain)
|
|
cfg.DeviceAuthorizationFlow.ProviderConfig.Domain = u.Host
|
|
|
|
if cfg.DeviceAuthorizationFlow.ProviderConfig.Scope == "" {
|
|
cfg.DeviceAuthorizationFlow.ProviderConfig.Scope = nbconfig.DefaultDeviceAuthFlowScope
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ApplyPKCEFlowConfig applies OIDC config to PKCEAuthorizationFlow if configured
|
|
func ApplyPKCEFlowConfig(ctx context.Context, cfg *nbconfig.Config, oidcConfig *OIDCConfigResponse) {
|
|
if cfg.PKCEAuthorizationFlow == nil {
|
|
return
|
|
}
|
|
log.WithContext(ctx).Infof("overriding PKCEAuthorizationFlow.TokenEndpoint with a new value: %s, previously configured value: %s",
|
|
oidcConfig.TokenEndpoint, cfg.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint)
|
|
cfg.PKCEAuthorizationFlow.ProviderConfig.TokenEndpoint = oidcConfig.TokenEndpoint
|
|
|
|
log.WithContext(ctx).Infof("overriding PKCEAuthorizationFlow.AuthorizationEndpoint with a new value: %s, previously configured value: %s",
|
|
oidcConfig.AuthorizationEndpoint, cfg.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint)
|
|
cfg.PKCEAuthorizationFlow.ProviderConfig.AuthorizationEndpoint = oidcConfig.AuthorizationEndpoint
|
|
}
|
|
|
|
// LogConfigInfo logs informational messages about the loaded configuration
|
|
func LogConfigInfo(cfg *nbconfig.Config) {
|
|
if cfg.EmbeddedIdP != nil {
|
|
log.Infof("running with the embedded IdP: %v", cfg.EmbeddedIdP.Issuer)
|
|
}
|
|
if cfg.Relay != nil {
|
|
log.Infof("Relay addresses: %v", cfg.Relay.Addresses)
|
|
}
|
|
}
|
|
|
|
// EnsureEncryptionKey generates and saves a DataStoreEncryptionKey if not set
|
|
func EnsureEncryptionKey(ctx context.Context, configPath string, cfg *nbconfig.Config) error {
|
|
if cfg.DataStoreEncryptionKey != "" {
|
|
return nil
|
|
}
|
|
|
|
log.WithContext(ctx).Infof("DataStoreEncryptionKey is not set, generating a new key")
|
|
key, err := crypt.GenerateKey()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate datastore encryption key: %v", err)
|
|
}
|
|
cfg.DataStoreEncryptionKey = key
|
|
|
|
if err := util.DirectWriteJson(ctx, configPath, cfg); err != nil {
|
|
return fmt.Errorf("failed to save config with new encryption key: %v", err)
|
|
}
|
|
log.WithContext(ctx).Infof("DataStoreEncryptionKey generated and saved to config")
|
|
return nil
|
|
}
|
|
|
|
// OIDCConfigResponse used for parsing OIDC config response
|
|
type OIDCConfigResponse struct {
|
|
Issuer string `json:"issuer"`
|
|
TokenEndpoint string `json:"token_endpoint"`
|
|
DeviceAuthEndpoint string `json:"device_authorization_endpoint"`
|
|
JwksURI string `json:"jwks_uri"`
|
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
|
}
|
|
|
|
// fetchOIDCConfig fetches OIDC configuration from the IDP
|
|
func fetchOIDCConfig(ctx context.Context, oidcEndpoint string) (OIDCConfigResponse, error) {
|
|
res, err := http.Get(oidcEndpoint)
|
|
if err != nil {
|
|
return OIDCConfigResponse{}, fmt.Errorf("failed fetching OIDC configuration from endpoint %s %v", oidcEndpoint, err)
|
|
}
|
|
|
|
defer func() {
|
|
err := res.Body.Close()
|
|
if err != nil {
|
|
log.WithContext(ctx).Debugf("failed closing response body %v", err)
|
|
}
|
|
}()
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return OIDCConfigResponse{}, fmt.Errorf("failed reading OIDC configuration response body: %v", err)
|
|
}
|
|
|
|
if res.StatusCode != 200 {
|
|
return OIDCConfigResponse{}, fmt.Errorf("OIDC configuration request returned status %d with response: %s",
|
|
res.StatusCode, string(body))
|
|
}
|
|
|
|
config := OIDCConfigResponse{}
|
|
err = json.Unmarshal(body, &config)
|
|
if err != nil {
|
|
return OIDCConfigResponse{}, fmt.Errorf("failed unmarshaling OIDC configuration response: %v", err)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func handleRebrand(cmd *cobra.Command) error {
|
|
var err error
|
|
if logFile == defaultLogFile {
|
|
if migrateToNetbird(oldDefaultLogFile, defaultLogFile) {
|
|
cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogDir, defaultLogDir)
|
|
err = cpDir(oldDefaultLogDir, defaultLogDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if nbconfig.MgmtConfigPath == defaultMgmtConfig {
|
|
if migrateToNetbird(oldDefaultMgmtConfig, defaultMgmtConfig) {
|
|
cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultMgmtConfigDir, defaultMgmtConfigDir)
|
|
err = cpDir(oldDefaultMgmtConfigDir, defaultMgmtConfigDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if mgmtDataDir == defaultMgmtDataDir {
|
|
if migrateToNetbird(oldDefaultMgmtDataDir, defaultMgmtDataDir) {
|
|
cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultMgmtDataDir, defaultMgmtDataDir)
|
|
err = cpDir(oldDefaultMgmtDataDir, defaultMgmtDataDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cpFile(src, dst string) error {
|
|
var err error
|
|
var srcfd *os.File
|
|
var dstfd *os.File
|
|
var srcinfo os.FileInfo
|
|
|
|
if srcfd, err = os.Open(src); err != nil {
|
|
return err
|
|
}
|
|
defer srcfd.Close()
|
|
|
|
if dstfd, err = os.Create(dst); err != nil {
|
|
return err
|
|
}
|
|
defer dstfd.Close()
|
|
|
|
if _, err = io.Copy(dstfd, srcfd); err != nil {
|
|
return err
|
|
}
|
|
if srcinfo, err = os.Stat(src); err != nil {
|
|
return err
|
|
}
|
|
return os.Chmod(dst, srcinfo.Mode())
|
|
}
|
|
|
|
func copySymLink(source, dest string) error {
|
|
link, err := os.Readlink(source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.Symlink(link, dest)
|
|
}
|
|
|
|
func cpDir(src string, dst string) error {
|
|
var err error
|
|
var fds []os.DirEntry
|
|
var srcinfo os.FileInfo
|
|
|
|
if srcinfo, err = os.Stat(src); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
|
|
return err
|
|
}
|
|
|
|
if fds, err = os.ReadDir(src); err != nil {
|
|
return err
|
|
}
|
|
for _, fd := range fds {
|
|
srcfp := path.Join(src, fd.Name())
|
|
dstfp := path.Join(dst, fd.Name())
|
|
|
|
fileInfo, err := os.Stat(srcfp)
|
|
if err != nil {
|
|
log.Fatalf("Couldn't get fileInfo; %v", err)
|
|
}
|
|
|
|
switch fileInfo.Mode() & os.ModeType {
|
|
case os.ModeSymlink:
|
|
if err = copySymLink(srcfp, dstfp); err != nil {
|
|
log.Fatalf("Failed to copy from %s to %s; %v", srcfp, dstfp, err)
|
|
}
|
|
case os.ModeDir:
|
|
if err = cpDir(srcfp, dstfp); err != nil {
|
|
log.Fatalf("Failed to copy from %s to %s; %v", srcfp, dstfp, err)
|
|
}
|
|
default:
|
|
if err = cpFile(srcfp, dstfp); err != nil {
|
|
log.Fatalf("Failed to copy from %s to %s; %v", srcfp, dstfp, err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func migrateToNetbird(oldPath, newPath string) bool {
|
|
_, errOld := os.Stat(oldPath)
|
|
_, errNew := os.Stat(newPath)
|
|
|
|
if errors.Is(errOld, fs.ErrNotExist) || errNew == nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|