diff --git a/cmd/flamenco-worker/main.go b/cmd/flamenco-worker/main.go index 01041c14..4395b0c0 100644 --- a/cmd/flamenco-worker/main.go +++ b/cmd/flamenco-worker/main.go @@ -4,6 +4,7 @@ package main import ( "context" + "errors" "flag" "fmt" "net/url" @@ -19,7 +20,9 @@ import ( "github.com/rs/zerolog/log" "git.blender.org/flamenco/internal/appinfo" + "git.blender.org/flamenco/internal/upnp_ssdp" "git.blender.org/flamenco/internal/worker" + "git.blender.org/flamenco/pkg/api" ) var ( @@ -58,6 +61,7 @@ func main() { configLogLevel() configWrangler := worker.NewConfigWrangler() + maybeAutodiscoverManager(&configWrangler) // Startup can take arbitrarily long, as it only ends when the Manager can be // reached and accepts our sign-on request. An offline Manager would cause the @@ -180,3 +184,99 @@ func upstreamBufferOrDie(client worker.FlamencoClient, timeService clock.Clock) return buffer } + +// maybeAutodiscoverManager starts Manager auto-discovery if there is no Manager URL configured yet. +func maybeAutodiscoverManager(configWrangler *worker.FileConfigWrangler) { + cfg, err := configWrangler.WorkerConfig() + if err != nil { + log.Fatal().Err(err).Msg("unable to load configuration") + } + + if cfg.ManagerURL != "" { + // Manager URL is already known, don't bother with auto-discovery. + return + } + + discoverCtx, discoverCancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer discoverCancel() + + foundManager, err := autodiscoverManager(discoverCtx) + if err != nil { + log.Fatal().Err(err).Msg("unable to discover manager") + } + + configWrangler.SetManagerURL(foundManager) +} + +func autodiscoverManager(ctx context.Context) (string, error) { + c, err := upnp_ssdp.NewClient(log.Logger) + if err != nil { + return "", fmt.Errorf("unable to create UPnP/SSDP client: %w", err) + } + + log.Info().Msg("auto-discovering Manager via UPnP/SSDP") + + urls, err := c.Run(ctx) + if err != nil { + return "", fmt.Errorf("unable to find Manager: %w", err) + } + + if len(urls) == 0 { + return "", errors.New("no Manager could be found") + } + + // Try out the URLs to see which one responds. + // TODO: parallelise this. + usableURLs := make([]string, 0) + for _, url := range urls { + if pingManager(ctx, url) { + usableURLs = append(usableURLs, url) + } + } + + switch len(usableURLs) { + case 0: + return "", fmt.Errorf("autodetected %d URLs, but none were usable", len(urls)) + + case 1: + log.Info().Str("url", usableURLs[0]).Msg("found Manager") + return usableURLs[0], nil + + default: + log.Info(). + Strs("urls", usableURLs). + Str("url", usableURLs[0]). + Msg("found multiple usable URLs, using the first one") + return usableURLs[0], nil + } +} + +// pingManager connects to a Manager and returns true if it responds. +func pingManager(ctx context.Context, url string) bool { + logger := log.With().Str("manager", url).Logger() + + client, err := api.NewClientWithResponses(url) + if err != nil { + logger.Warn().Err(err).Msg("unable to create API client with this URL") + return false + } + + resp, err := client.GetVersionWithResponse(ctx) + if err != nil { + logger.Warn().Err(err).Msg("unable to get Flamenco version from Manager") + return false + } + + if resp.JSON200 == nil { + logger.Warn(). + Int("httpStatus", resp.StatusCode()). + Msg("unable to get Flamenco version, unexpected reply") + return false + } + + logger.Info(). + Str("version", resp.JSON200.Version). + Str("name", resp.JSON200.Name). + Msg("found Flamenco Manager") + return true +}