Skip to content

Commit

Permalink
add api for config reload & config update
Browse files Browse the repository at this point in the history
  • Loading branch information
0x2142 committed Nov 19, 2024
1 parent f672f1a commit c98a9f6
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 26 deletions.
74 changes: 73 additions & 1 deletion api/v1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"

"github.com/0x2142/frigate-notify/config"
"github.com/0x2142/frigate-notify/events"
"github.com/danielgtaylor/huma/v2"
"github.com/rs/zerolog/log"
)

Expand All @@ -13,7 +15,22 @@ type ConfigOutput struct {
}
}

// GetConfig returns the current running configuratio
type PutConfigInput struct {
Body struct {
Config config.Config `json:"config"`
SkipSave bool `json:"skipsave,omitempty" doc:"Skip writing new config to file" default:"false"`
SkipBackup bool `json:"skipbackup,omitempty" doc:"Skip creating config file backup" default:"false"`
}
}

type PutConfigOutput struct {
Body struct {
Status string `json:"status"`
Errors []string `json:"errors,omitempty"`
}
}

// GetConfig returns the current running configuration
func GetConfig(ctx context.Context, input *struct{}) (*ConfigOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/config").
Expand All @@ -30,3 +47,58 @@ func GetConfig(ctx context.Context, input *struct{}) (*ConfigOutput, error) {

return resp, nil
}

// PutConfig replaces the current running configuration
func PutConfig(ctx context.Context, input *PutConfigInput) (*PutConfigOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/config").
Str("method", "PUT").
Msg("Received API request")

resp := &PutConfigOutput{}

newConfig := input.Body.Config
validationErrors := newConfig.Validate()

if len(validationErrors) == 0 {
resp.Body.Status = "ok"
go reloadCfg(newConfig, input.Body.SkipSave, input.Body.SkipBackup)

log.Trace().
Str("uri", V1_PREFIX+"/config").
Interface("response_json", resp.Body).
Msg("Sent API response")
return resp, nil
} else {
resp.Body.Status = "validation error"
resp.Body.Errors = validationErrors

log.Trace().
Str("uri", V1_PREFIX+"/config").
Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, huma.Error422UnprocessableEntity("config validation failed")
}
}

func reloadCfg(newconfig config.Config, skipSave bool, skipBackup bool) {
log.Info().Msg("Reloading app config...")
log.Trace().
Bool("skipSave", skipSave).
Bool("skipBackup", skipBackup).
Msg("Config reload via API")
if config.ConfigData.Frigate.MQTT.Enabled {
events.DisconnectMQTT()
}

config.ConfigData = newconfig
if !skipSave {
config.Save(skipBackup)
}

if config.ConfigData.Frigate.MQTT.Enabled {
events.SubscribeMQTT()
}
log.Info().Msg("Config reload completed")
}
40 changes: 40 additions & 0 deletions api/v1/reload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package apiv1

import (
"context"

"github.com/0x2142/frigate-notify/config"
"github.com/rs/zerolog/log"
)

type ReloadOutput struct {
Body struct {
Message string `json:"message"`
}
}

// PostReload reloads current app config
func PostReload(ctx context.Context, input *struct{}) (*ReloadOutput, error) {
log.Trace().
Str("uri", V1_PREFIX+"/reload").
Str("method", "POST").
Msg("Received API request")

resp := &ReloadOutput{}
resp.Body.Message = "ok"

go func() {
log.Info().Msg("Received request to reload config")
// Re-load from file & trigger reload
config.Load()
newconfig := config.ConfigData
go reloadCfg(newconfig, true, true)
}()

log.Trace().
Str("uri", V1_PREFIX+"/reload").
Interface("response_json", resp.Body).
Msg("Sent API response")

return resp, nil
}
22 changes: 22 additions & 0 deletions api/v1/v1_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ func Registerv1Routes(api huma.API) {
Tags: []string{"Config"},
}, GetConfig)

// PUT /config
huma.Register(api, huma.Operation{
OperationID: "put-config",
Method: http.MethodPut,
Path: V1_PREFIX + "/config",
Summary: V1_PREFIX + "/config",
Description: "Set current running configuration",
Tags: []string{"Config"},
DefaultStatus: 202,
}, PutConfig)

// POST /reload
huma.Register(api, huma.Operation{
OperationID: "post-reload",
Method: http.MethodPost,
Path: V1_PREFIX + "/reload",
Summary: V1_PREFIX + "/reload",
Description: "Reload config from file & restart app",
Tags: []string{"Control"},
DefaultStatus: 202,
}, PostReload)

// GET /notif_state
huma.Register(api, huma.Operation{
OperationID: "get-notif-state",
Expand Down
66 changes: 54 additions & 12 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,42 @@ package config
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"time"

"github.com/0x2142/frigate-notify/models"
"github.com/kkyr/fig"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)

type Config struct {
App models.App `fig:"app" json:"app"`
Frigate *models.Frigate `fig:"frigate" json:"frigate" validate:"required"`
Alerts *models.Alerts `fig:"alerts" json:"alerts" validate:"required"`
Monitor models.Monitor `fig:"monitor" json:"monitor"`
App models.App `fig:"app" json:"app" required:"false"`
Frigate *models.Frigate `fig:"frigate" json:"frigate" validate:"required" required:"true"`
Alerts *models.Alerts `fig:"alerts" json:"alerts" validate:"required" required:"true"`
Monitor models.Monitor `fig:"monitor" json:"monitor" required:"false"`
}

var ConfigData Config
var ConfigFile string

// loadConfig opens & attempts to parse configuration file
func LoadConfig(configFile string) {
// Load opens & attempts to parse configuration file
func Load() {
// Set config file location
if configFile == "" {
if ConfigFile == "" {
var ok bool
configFile, ok = os.LookupEnv("FN_CONFIGFILE")
ConfigFile, ok = os.LookupEnv("FN_CONFIGFILE")
if !ok {
configFile = "./config.yml"
ConfigFile = "./config.yml"
}
}

// Load Config file
log.Debug().Msgf("Attempting to load config file: %v", configFile)
log.Debug().Msgf("Attempting to load config file: %v", ConfigFile)

err := fig.Load(&ConfigData, fig.File(filepath.Base(configFile)), fig.Dirs(filepath.Dir(configFile)), fig.UseEnv("FN"))
err := fig.Load(&ConfigData, fig.File(filepath.Base(ConfigFile)), fig.Dirs(filepath.Dir(ConfigFile)), fig.UseEnv("FN"))
if err != nil {
if errors.Is(err, fig.ErrFileNotFound) {
log.Warn().Msg("Config file could not be read, attempting to load config from environment")
Expand All @@ -53,7 +57,7 @@ func LoadConfig(configFile string) {
log.Info().Msg("Config loaded.")

// Send config file to validation before completing
validationErrors := ConfigData.validate()
validationErrors := ConfigData.Validate()

if len(validationErrors) > 0 {
fmt.Println()
Expand All @@ -67,3 +71,41 @@ func LoadConfig(configFile string) {
log.Info().Msg("Config file validated!")
}
}

func Save(skipBackup bool) {
log.Debug().Msg("Writing new config file")

data, err := yaml.Marshal(&ConfigData)
if err != nil {
log.Error().Err(err).Msg("Unable to save config")
return
}

// Store backup of original config, if requested
if !skipBackup {
original, err := os.Open(ConfigFile)
if err != nil {
log.Error().Err(err).Msg("Unable to create config backup")
}
defer original.Close()

newFile := fmt.Sprintf("%s-%s.bak", ConfigFile, time.Now().Format("20060102150405"))
copy, err := os.Create(newFile)
if err != nil {
log.Error().Err(err).Msg("Unable to create config backup")
}
defer copy.Close()

io.Copy(copy, original)
log.Info().Msgf("Created config file backup: %v", newFile)

}

err = os.WriteFile(ConfigFile, data, 0644)
if err != nil {
log.Error().Err(err).Msg("Unable to save config")
return
}

log.Info().Msg("Config file saved")
}
5 changes: 3 additions & 2 deletions config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/rs/zerolog/log"
)

func (c *Config) validate() []string {
func (c *Config) Validate() []string {
var validationErrors []string
log.Debug().Msg("Validating config file...")

Expand Down Expand Up @@ -250,9 +250,10 @@ func (c *Config) validateFrigateConnectivity() []string {
}
if current_attempt == max_attempts {
Internal.Status.Frigate.API = "unreachable"
log.Fatal().
log.Error().
Err(err).
Msgf("Max attempts reached - Cannot reach Frigate server at %v", url)
connectivityErrors = append(connectivityErrors, "Max attempts reached - Cannot reach Frigate server at "+url)
}
var stats models.FrigateStats
json.Unmarshal([]byte(response), &stats)
Expand Down
10 changes: 9 additions & 1 deletion events/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
)

var mqtt_topic string
var client mqtt.Client

// SubscribeMQTT establishes subscription to MQTT server & listens for messages
func SubscribeMQTT() {
Expand Down Expand Up @@ -51,7 +52,7 @@ func SubscribeMQTT() {
log.Fatal().Msgf("Max retries exceeded. Failed to establish MQTT session to %s", config.ConfigData.Frigate.MQTT.Server)
}
// Connect to MQTT broker
client := mqtt.NewClient(opts)
client = mqtt.NewClient(opts)

if token := client.Connect(); token.Wait() && token.Error() != nil {
retry += 1
Expand All @@ -67,6 +68,13 @@ func SubscribeMQTT() {
}
}

// disconnectMQTT simply disconnects the MQTT client
func DisconnectMQTT() {
log.Info().Msg("Ending MQTT session")
client.Disconnect(300)
log.Info().Msg("MQTT disconnected")
}

// connectionLostHandler logs error message on MQTT connection loss
func connectionLostHandler(c mqtt.Client, err error) {
config.Internal.Status.Health = "frigate mqtt connection lost"
Expand Down
7 changes: 4 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ func main() {
log.Info().Msg("Starting...")

// Load & validate config
config.LoadConfig(configFile)
config.ConfigFile = configFile
config.Load()

notifier.TemplateFiles = NotifTemplates

Expand All @@ -110,6 +111,7 @@ func main() {

// Set up event cache
events.InitZoneCache()
defer events.CloseZoneCache()

// Start API server if enabled
if config.ConfigData.App.API.Enabled {
Expand All @@ -135,10 +137,9 @@ func main() {

// Connect MQTT
if config.ConfigData.Frigate.MQTT.Enabled {
defer events.CloseZoneCache()

log.Debug().Msg("Connecting to MQTT Server...")
events.SubscribeMQTT()
defer events.DisconnectMQTT()
log.Info().Msg("App ready!")
config.Internal.Status.Health = "ok"
sig := make(chan os.Signal, 1)
Expand Down
Loading

0 comments on commit c98a9f6

Please sign in to comment.