2024-03-01 15:19:53 +00:00
|
|
|
package startup
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"path"
|
2024-04-25 20:06:22 +00:00
|
|
|
"path/filepath"
|
2024-04-20 00:31:15 +00:00
|
|
|
"time"
|
2024-03-01 15:19:53 +00:00
|
|
|
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
|
|
"github.com/go-skynet/LocalAI/core/config"
|
|
|
|
"github.com/imdario/mergo"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
)
|
|
|
|
|
2024-04-18 03:21:55 +00:00
|
|
|
type fileHandler func(fileContent []byte, appConfig *config.ApplicationConfig) error
|
|
|
|
|
|
|
|
type configFileHandler struct {
|
|
|
|
handlers map[string]fileHandler
|
|
|
|
|
|
|
|
watcher *fsnotify.Watcher
|
|
|
|
|
|
|
|
appConfig *config.ApplicationConfig
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|
|
|
|
|
2024-04-18 03:21:55 +00:00
|
|
|
// TODO: This should be a singleton eventually so other parts of the code can register config file handlers,
|
|
|
|
// then we can export it to other packages
|
|
|
|
func newConfigFileHandler(appConfig *config.ApplicationConfig) configFileHandler {
|
|
|
|
c := configFileHandler{
|
|
|
|
handlers: make(map[string]fileHandler),
|
|
|
|
appConfig: appConfig,
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|
2024-04-29 13:11:42 +00:00
|
|
|
err := c.Register("api_keys.json", readApiKeysJson(*appConfig), true)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Str("file", "api_keys.json").Msg("unable to register config file handler")
|
|
|
|
}
|
|
|
|
err = c.Register("external_backends.json", readExternalBackendsJson(*appConfig), true)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Str("file", "external_backends.json").Msg("unable to register config file handler")
|
|
|
|
}
|
2024-04-18 03:21:55 +00:00
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *configFileHandler) Register(filename string, handler fileHandler, runNow bool) error {
|
|
|
|
_, ok := c.handlers[filename]
|
|
|
|
if ok {
|
|
|
|
return fmt.Errorf("handler already registered for file %s", filename)
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|
2024-04-18 03:21:55 +00:00
|
|
|
c.handlers[filename] = handler
|
|
|
|
if runNow {
|
2024-04-26 16:46:35 +00:00
|
|
|
c.callHandler(filename, handler)
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-04-18 03:21:55 +00:00
|
|
|
func (c *configFileHandler) callHandler(filename string, handler fileHandler) {
|
2024-04-26 16:46:35 +00:00
|
|
|
rootedFilePath := filepath.Join(c.appConfig.DynamicConfigsDir, filepath.Clean(filename))
|
|
|
|
log.Trace().Str("filename", rootedFilePath).Msg("reading file for dynamic config update")
|
|
|
|
fileContent, err := os.ReadFile(rootedFilePath)
|
2024-04-18 03:21:55 +00:00
|
|
|
if err != nil && !os.IsNotExist(err) {
|
2024-04-26 16:46:35 +00:00
|
|
|
log.Error().Err(err).Str("filename", rootedFilePath).Msg("could not read file")
|
2024-04-18 03:21:55 +00:00
|
|
|
}
|
2024-03-01 15:19:53 +00:00
|
|
|
|
2024-04-18 03:21:55 +00:00
|
|
|
if err = handler(fileContent, c.appConfig); err != nil {
|
|
|
|
log.Error().Err(err).Msg("WatchConfigDirectory goroutine failed to update options")
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|
2024-04-18 03:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *configFileHandler) Watch() error {
|
2024-03-01 15:19:53 +00:00
|
|
|
configWatcher, err := fsnotify.NewWatcher()
|
2024-04-18 03:21:55 +00:00
|
|
|
c.watcher = configWatcher
|
2024-03-01 15:19:53 +00:00
|
|
|
if err != nil {
|
2024-05-26 12:48:30 +00:00
|
|
|
return err
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|
|
|
|
|
2024-04-20 00:31:15 +00:00
|
|
|
if c.appConfig.DynamicConfigsDirPollInterval > 0 {
|
|
|
|
log.Debug().Msg("Poll interval set, falling back to polling for configuration changes")
|
|
|
|
ticker := time.NewTicker(c.appConfig.DynamicConfigsDirPollInterval)
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
<-ticker.C
|
|
|
|
for file, handler := range c.handlers {
|
|
|
|
log.Debug().Str("file", file).Msg("polling config file")
|
2024-04-26 16:46:35 +00:00
|
|
|
c.callHandler(file, handler)
|
2024-04-20 00:31:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2024-03-01 15:19:53 +00:00
|
|
|
// Start listening for events.
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
2024-04-18 03:21:55 +00:00
|
|
|
case event, ok := <-c.watcher.Events:
|
2024-03-01 15:19:53 +00:00
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
2024-04-18 03:21:55 +00:00
|
|
|
if event.Has(fsnotify.Write | fsnotify.Create | fsnotify.Remove) {
|
|
|
|
handler, ok := c.handlers[path.Base(event.Name)]
|
|
|
|
if !ok {
|
|
|
|
continue
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|
2024-04-18 03:21:55 +00:00
|
|
|
|
2024-04-26 16:46:35 +00:00
|
|
|
c.callHandler(filepath.Base(event.Name), handler)
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|
2024-04-18 03:21:55 +00:00
|
|
|
case err, ok := <-c.watcher.Errors:
|
|
|
|
log.Error().Err(err).Msg("config watcher error received")
|
2024-03-01 15:19:53 +00:00
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
// Add a path.
|
2024-04-18 03:21:55 +00:00
|
|
|
err = c.watcher.Add(c.appConfig.DynamicConfigsDir)
|
2024-03-01 15:19:53 +00:00
|
|
|
if err != nil {
|
2024-04-18 03:21:55 +00:00
|
|
|
return fmt.Errorf("unable to establish watch on the LocalAI Configuration Directory: %+v", err)
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|
|
|
|
|
2024-04-18 03:21:55 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: When we institute graceful shutdown, this should be called
|
2024-04-29 13:11:42 +00:00
|
|
|
func (c *configFileHandler) Stop() error {
|
|
|
|
return c.watcher.Close()
|
2024-04-18 03:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func readApiKeysJson(startupAppConfig config.ApplicationConfig) fileHandler {
|
|
|
|
handler := func(fileContent []byte, appConfig *config.ApplicationConfig) error {
|
2024-04-25 20:06:22 +00:00
|
|
|
log.Debug().Msg("processing api keys runtime update")
|
|
|
|
log.Trace().Int("numKeys", len(startupAppConfig.ApiKeys)).Msg("api keys provided at startup")
|
2024-04-18 03:21:55 +00:00
|
|
|
|
|
|
|
if len(fileContent) > 0 {
|
|
|
|
// Parse JSON content from the file
|
|
|
|
var fileKeys []string
|
|
|
|
err := json.Unmarshal(fileContent, &fileKeys)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-04-25 20:06:22 +00:00
|
|
|
log.Trace().Int("numKeys", len(fileKeys)).Msg("discovered API keys from api keys dynamic config dile")
|
|
|
|
|
2024-04-18 03:21:55 +00:00
|
|
|
appConfig.ApiKeys = append(startupAppConfig.ApiKeys, fileKeys...)
|
|
|
|
} else {
|
2024-04-25 20:06:22 +00:00
|
|
|
log.Trace().Msg("no API keys discovered from dynamic config file")
|
2024-04-18 03:21:55 +00:00
|
|
|
appConfig.ApiKeys = startupAppConfig.ApiKeys
|
|
|
|
}
|
2024-04-25 20:06:22 +00:00
|
|
|
log.Trace().Int("numKeys", len(appConfig.ApiKeys)).Msg("total api keys after processing")
|
2024-04-18 03:21:55 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return handler
|
|
|
|
}
|
|
|
|
|
|
|
|
func readExternalBackendsJson(startupAppConfig config.ApplicationConfig) fileHandler {
|
|
|
|
handler := func(fileContent []byte, appConfig *config.ApplicationConfig) error {
|
|
|
|
log.Debug().Msg("processing external_backends.json")
|
|
|
|
|
|
|
|
if len(fileContent) > 0 {
|
|
|
|
// Parse JSON content from the file
|
|
|
|
var fileBackends map[string]string
|
|
|
|
err := json.Unmarshal(fileContent, &fileBackends)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
appConfig.ExternalGRPCBackends = startupAppConfig.ExternalGRPCBackends
|
|
|
|
err = mergo.Merge(&appConfig.ExternalGRPCBackends, &fileBackends)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
appConfig.ExternalGRPCBackends = startupAppConfig.ExternalGRPCBackends
|
|
|
|
}
|
|
|
|
log.Debug().Msg("external backends loaded from external_backends.json")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return handler
|
2024-03-01 15:19:53 +00:00
|
|
|
}
|