Worker: change how configuration & credentials are loaded/saved

The Worker config/credential management was a bit of a mess. It's now
better structured, and also allows runtime overrides of the Manager URL,
without writing that override to the config file.
This commit is contained in:
Sybren A. Stüvel 2022-03-08 13:51:03 +01:00
parent c0cd3ca5ad
commit 42407865eb
2 changed files with 86 additions and 54 deletions

View File

@ -8,6 +8,7 @@ import (
"io"
"net/url"
"os"
"strings"
"time"
"github.com/rs/zerolog/log"
@ -26,7 +27,13 @@ const (
// WorkerConfig represents the configuration of a single worker.
// It does not include authentication credentials.
type WorkerConfig struct {
Manager string `yaml:"manager_url"`
// ConfiguredManager is the Manager URL that's in the configuration file.
ConfiguredManager string `yaml:"manager_url"`
// ManagerURL is the Manager URL to use by the Worker. It could come from the
// configuration file, but also from autodiscovery via UPnP/SSDP.
ManagerURL string `yaml:"-"`
TaskTypes []string `yaml:"task_types"`
}
@ -35,42 +42,59 @@ type WorkerCredentials struct {
Secret string `yaml:"worker_secret"`
}
func loadConfig(configWrangler FileConfigWrangler) (WorkerConfig, error) {
logger := log.With().Str("filename", configFilename).Logger()
// FileConfigWrangler is the default config wrangler that actually reads & writes files.
type FileConfigWrangler struct {
// In-memory copy of the worker configuration.
wc *WorkerConfig
creds *WorkerCredentials
}
var cfg WorkerConfig
// NewConfigWrangler returns ConfigWrangler that reads files.
func NewConfigWrangler() FileConfigWrangler {
return FileConfigWrangler{}
}
err := configWrangler.LoadConfig(configFilename, &cfg)
// WorkerConfig returns the worker configuration, or the default config if
// there is no config file. Configuration is only loaded from disk once;
// subsequent calls return the same config.
func (fcw *FileConfigWrangler) WorkerConfig() (WorkerConfig, error) {
if fcw.wc != nil {
return *fcw.wc, nil
}
wc := WorkerConfig{}
err := fcw.loadConfig(configFilename, &wc)
// If the configuration file doesn't exist, write the defaults & retry loading them.
if os.IsNotExist(err) {
logger.Info().Msg("writing default configuration file")
cfg = configWrangler.DefaultConfig()
err = configWrangler.WriteConfig(configFilename, "Configuration", cfg)
if err != nil {
return cfg, fmt.Errorf("writing default config: %w", err)
if !os.IsNotExist(err) {
return WorkerConfig{}, err
}
err = configWrangler.LoadConfig(configFilename, &cfg)
// The config file not existing is fine; just use the defaults.
wc = fcw.DefaultConfig()
}
fcw.wc = &wc
man := strings.TrimSpace(wc.ConfiguredManager)
if man != "" {
fcw.SetManagerURL(man)
}
return wc, nil
}
func (fcw *FileConfigWrangler) SaveConfig() error {
err := fcw.writeConfig(configFilename, "Configuration", fcw.wc)
if err != nil {
return cfg, fmt.Errorf("loading config from %s: %w", configFilename, err)
return fmt.Errorf("writing to %s: %w", configFilename, err)
}
return nil
}
// Validate the manager URL.
if cfg.Manager != "" {
_, err := ParseURL(cfg.Manager)
if err != nil {
return cfg, fmt.Errorf("parsing manager URL %s: %w", cfg.Manager, err)
}
logger.Debug().Str("url", cfg.Manager).Msg("parsed manager URL")
}
return cfg, nil
}
func loadCredentials(configWrangler FileConfigWrangler) (WorkerCredentials, error) {
func (fcw *FileConfigWrangler) WorkerCredentials() (WorkerCredentials, error) {
var creds WorkerCredentials
err := configWrangler.LoadConfig(credentialsFilename, &creds)
err := fcw.loadConfig(credentialsFilename, &creds)
if err != nil {
return WorkerCredentials{}, err
}
@ -81,24 +105,34 @@ func loadCredentials(configWrangler FileConfigWrangler) (WorkerCredentials, erro
return creds, nil
}
// FileConfigWrangler is the default config wrangler that actually reads & writes files.
type FileConfigWrangler struct{}
func (fcw *FileConfigWrangler) SaveCredentials(creds WorkerCredentials) error {
fcw.creds = &creds
// NewConfigWrangler returns ConfigWrangler that reads files.
func NewConfigWrangler() FileConfigWrangler {
return FileConfigWrangler{}
err := fcw.writeConfig(credentialsFilename, "Credentials", creds)
if err != nil {
return fmt.Errorf("writing to %s: %w", credentialsFilename, err)
}
return nil
}
// SetManagerURL overwrites the Manager URL in the cached configuration.
// This is an in-memory change only, and will not be written to the config file.
// Returns a new copy of the WorkerConfig with the Manager URL updated.
func (fcw *FileConfigWrangler) SetManagerURL(managerURL string) WorkerConfig {
fcw.wc.ManagerURL = managerURL
return *fcw.wc
}
// DefaultConfig returns a fairly sane default configuration.
func (fcw FileConfigWrangler) DefaultConfig() WorkerConfig {
return WorkerConfig{
Manager: "",
ConfiguredManager: "", // Auto-detect by default.
TaskTypes: []string{"blender", "file-management", "exr-merge", "misc"},
}
}
// WriteConfig stores a struct as YAML file.
func (fcw FileConfigWrangler) WriteConfig(filename string, filetype string, config interface{}) error {
func (fcw FileConfigWrangler) writeConfig(filename string, filetype string, config interface{}) error {
data, err := yaml.Marshal(config)
if err != nil {
return err
@ -144,7 +178,7 @@ func (fcw FileConfigWrangler) WriteConfig(filename string, filetype string, conf
}
// LoadConfig loads a YAML configuration file into 'config'
func (fcw FileConfigWrangler) LoadConfig(filename string, config interface{}) error {
func (fcw FileConfigWrangler) loadConfig(filename string, config interface{}) error {
log.Debug().Str("filename", filename).Msg("loading config file")
f, err := os.OpenFile(filename, os.O_RDONLY, 0)
if err != nil {

View File

@ -30,19 +30,18 @@ func RegisterOrSignOn(ctx context.Context, configWrangler FileConfigWrangler) (
client FlamencoClient, startupState api.WorkerStatus,
) {
// Load configuration
cfg, err := loadConfig(configWrangler)
cfg, err := configWrangler.WorkerConfig()
if err != nil {
log.Fatal().Err(err).Msg("loading configuration")
}
log.Info().Interface("config", cfg).Msg("loaded configuration")
if cfg.Manager == "" {
log.Fatal().Msg("no manager configured")
if cfg.ManagerURL == "" {
log.Fatal().Msg("no Manager configured")
}
// Load credentials
creds, err := loadCredentials(configWrangler)
creds, err := configWrangler.WorkerCredentials()
if err == nil {
// Credentials can be loaded just fine, try to sign on with them.
client = authenticatedClient(cfg, creds)
@ -58,17 +57,16 @@ func RegisterOrSignOn(ctx context.Context, configWrangler FileConfigWrangler) (
creds = register(ctx, cfg, client)
// store ID and secretKey in config file when registration is complete.
err = configWrangler.WriteConfig(credentialsFilename, "Credentials", creds)
err = configWrangler.SaveCredentials(creds)
if err != nil {
log.Fatal().Err(err).Str("file", credentialsFilename).
Msg("unable to write credentials configuration file")
log.Fatal().Err(err).Msg("unable to write credentials file")
}
// Sign-on should work now.
client = authenticatedClient(cfg, creds)
startupState, err = signOn(ctx, cfg, client)
if err != nil {
log.Fatal().Err(err).Str("manager", cfg.Manager).Msg("unable to sign on after registering")
log.Fatal().Err(err).Str("manager", cfg.ManagerURL).Msg("unable to sign on after registering")
}
return
@ -141,7 +139,7 @@ func repeatSignOnUntilAnswer(ctx context.Context, cfg WorkerConfig, client Flame
// signOn tells the Manager we're alive and returns the status the Manager tells us to go to.
func signOn(ctx context.Context, cfg WorkerConfig, client FlamencoClient) (api.WorkerStatus, error) {
logger := log.With().Str("manager", cfg.Manager).Logger()
logger := log.With().Str("manager", cfg.ManagerURL).Logger()
req := api.SignOnJSONRequestBody{
Nickname: mustHostname(),
@ -191,7 +189,7 @@ func mustHostname() string {
// authenticatedClient constructs a Flamenco client with the given credentials.
func authenticatedClient(cfg WorkerConfig, creds WorkerCredentials) FlamencoClient {
flamenco, err := api.NewClientWithResponses(
cfg.Manager,
cfg.ManagerURL,
// Add a Basic HTTP authentication header to every request to Flamenco Manager.
api.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {