diff --git a/go.mod b/go.mod index ba80e2c..db6294c 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,9 @@ require ( github.com/go-playground/validator/v10 v10.11.2 // indirect github.com/klauspost/compress v1.10.3 // indirect github.com/leodido/go-urn v1.2.1 // indirect + github.com/r3labs/diff/v3 v3.0.1 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/crypto v0.5.0 // indirect golang.org/x/sys v0.4.0 // indirect golang.org/x/text v0.6.0 // indirect diff --git a/go.sum b/go.sum index 3dda176..0260a45 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= +github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -56,6 +58,10 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s= go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= diff --git a/internal/config/apply.go b/internal/config/apply.go new file mode 100644 index 0000000..fa2f81e --- /dev/null +++ b/internal/config/apply.go @@ -0,0 +1,41 @@ +package config + +import ( + "fmt" + + "golang.org/x/exp/slog" + "nfsense.net/nfsense/internal/definitions" +) + +// ApplyPendingChanges Takes all pending Changes and Tries to Apply them using the Registered Apply Functions. +// In Case of error it Attempts to Revert to the Current Config +func (m *ConfigManager) ApplyPendingChanges() error { + slog.Info("Applying Pending Changes...") + for _, fn := range m.applyFunctions { + err := fn(*m.pendingConfig) + if err != nil { + slog.Error("Applying Pending Changes", err) + err2 := revertToCurrent(m) + if err2 != nil { + slog.Error("Reverting Error", err2) + return fmt.Errorf("Apply Error %w; Reverting Error %w", err, err2) + } + return err + } + } + return nil +} + +func revertToCurrent(m *ConfigManager) error { + for _, fn := range m.applyFunctions { + err := fn(*m.currentConfig) + if err != nil { + return err + } + } + return nil +} + +func (m *ConfigManager) RegisterApplyFunction(fn func(definitions.Config) error) { + m.applyFunctions = append(m.applyFunctions, fn) +} diff --git a/internal/config/diff.go b/internal/config/diff.go new file mode 100644 index 0000000..1def515 --- /dev/null +++ b/internal/config/diff.go @@ -0,0 +1,11 @@ +package config + +import "github.com/r3labs/diff/v3" + +func (m *ConfigManager) AreChangesPending() bool { + return diff.Changed(m.currentConfig, m.pendingConfig) +} + +func (m *ConfigManager) GetPendingChangelog() (diff.Changelog, error) { + return diff.Diff(m.currentConfig, m.pendingConfig) +} diff --git a/internal/config/discard.go b/internal/config/discard.go new file mode 100644 index 0000000..2d4f230 --- /dev/null +++ b/internal/config/discard.go @@ -0,0 +1,6 @@ +package config + +func (m *ConfigManager) DiscardPendingConfig() error { + m.pendingConfig = m.currentConfig.Clone() + return nil +} diff --git a/internal/config/get.go b/internal/config/get.go new file mode 100644 index 0000000..f778196 --- /dev/null +++ b/internal/config/get.go @@ -0,0 +1,11 @@ +package config + +import "nfsense.net/nfsense/internal/definitions" + +func (m *ConfigManager) GetCurrentConfig() definitions.Config { + return *m.currentConfig.Clone() +} + +func (m *ConfigManager) GetPendingConfig() definitions.Config { + return *m.pendingConfig.Clone() +} diff --git a/internal/config/load.go b/internal/config/load.go new file mode 100644 index 0000000..1bd064a --- /dev/null +++ b/internal/config/load.go @@ -0,0 +1,57 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + + "nfsense.net/nfsense/internal/definitions" +) + +func (m *ConfigManager) LoadCurrentConfigFromDisk() error { + var config definitions.Config + configFile, err := os.Open(m.currentConfigFilePath) + if err != nil { + return fmt.Errorf("opening Config File %w", err) + } + defer configFile.Close() + + jsonParser := json.NewDecoder(configFile) + jsonParser.DisallowUnknownFields() + err = jsonParser.Decode(&config) + if err != nil { + return fmt.Errorf("decoding Config File %w", err) + } + + err = definitions.ValidateConfig(&config) + if err != nil { + return fmt.Errorf("validating Config: %w", err) + } + + m.currentConfig = &config + return nil +} + +func (m *ConfigManager) LoadPendingConfigFromDisk() error { + var config definitions.Config + configFile, err := os.Open(m.pendingConfigFilePath) + if err != nil { + return fmt.Errorf("opening Config File %w", err) + } + defer configFile.Close() + + jsonParser := json.NewDecoder(configFile) + jsonParser.DisallowUnknownFields() + err = jsonParser.Decode(&config) + if err != nil { + return fmt.Errorf("decoding Config File %w", err) + } + + err = definitions.ValidateConfig(&config) + if err != nil { + return fmt.Errorf("validating Config: %w", err) + } + + m.pendingConfig = &config + return nil +} diff --git a/internal/config/manager.go b/internal/config/manager.go new file mode 100644 index 0000000..9b09f50 --- /dev/null +++ b/internal/config/manager.go @@ -0,0 +1,29 @@ +package config + +import ( + "sync" + + "nfsense.net/nfsense/internal/definitions" +) + +type ConfigManager struct { + currentConfigFilePath string + pendingConfigFilePath string + + currentConfig *definitions.Config + pendingConfig *definitions.Config + + transactionMutex sync.Mutex + + applyFunctions []func(definitions.Config) error +} + +func CreateConfigManager() *ConfigManager { + manager := ConfigManager{ + currentConfigFilePath: "config.json", + pendingConfigFilePath: "pending.json", + currentConfig: &definitions.Config{}, + pendingConfig: &definitions.Config{}, + } + return &manager +} diff --git a/internal/config/transaction.go b/internal/config/transaction.go new file mode 100644 index 0000000..3338fa0 --- /dev/null +++ b/internal/config/transaction.go @@ -0,0 +1,55 @@ +package config + +import ( + "fmt" + "sync" + + "nfsense.net/nfsense/internal/definitions" +) + +type ConfigTransaction struct { + finished bool + mutex sync.Mutex + configManager *ConfigManager + changes *definitions.Config +} + +func (m *ConfigManager) StartTransaction() (*ConfigTransaction, *definitions.Config) { + m.transactionMutex.Lock() + confCopy := m.pendingConfig.Clone() + return &ConfigTransaction{ + configManager: m, + changes: confCopy, + }, confCopy +} + +func (t *ConfigTransaction) Commit() error { + t.mutex.Lock() + defer t.mutex.Unlock() + + if t.finished { + return fmt.Errorf("transaction already finished") + } + + t.finished = true + defer t.configManager.transactionMutex.Unlock() + + err := definitions.ValidateConfig(t.changes) + if err != nil { + return fmt.Errorf("validating Config before Apply: %w", err) + } + + t.configManager.pendingConfig = t.changes.Clone() + + return nil +} + +func (t *ConfigTransaction) Discard() { + t.mutex.Lock() + defer t.mutex.Unlock() + + if !t.finished { + t.finished = true + t.configManager.transactionMutex.Unlock() + } +}