2022-02-17 10:22:45 +00:00
|
|
|
package worker
|
2022-01-31 14:01:51 +00:00
|
|
|
|
2022-03-07 14:26:46 +00:00
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
2022-01-31 14:01:51 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"runtime"
|
2022-06-17 14:24:03 +00:00
|
|
|
"strings"
|
2022-02-15 10:44:16 +00:00
|
|
|
"time"
|
2022-01-31 14:01:51 +00:00
|
|
|
|
|
|
|
"github.com/rs/zerolog/log"
|
2022-03-01 19:45:09 +00:00
|
|
|
|
|
|
|
"git.blender.org/flamenco/internal/appinfo"
|
|
|
|
"git.blender.org/flamenco/pkg/api"
|
2022-01-31 14:01:51 +00:00
|
|
|
)
|
|
|
|
|
2022-06-17 14:24:03 +00:00
|
|
|
const workerNameEnvVariable = "FLAMENCO_WORKER_NAME"
|
|
|
|
|
2022-02-15 10:44:16 +00:00
|
|
|
var (
|
|
|
|
errSignOnCanceled = errors.New("sign-on cancelled") // For example by closing the context.
|
|
|
|
errSignOnRepeatableFailure = errors.New("unable to sign on at Manager, try again later") // For example failed connections
|
|
|
|
errSignOnRejected = errors.New("manager rejected our sign-on credentials") // Reached Manager, but it rejected our creds.
|
|
|
|
)
|
2022-01-31 14:01:51 +00:00
|
|
|
|
2022-02-17 10:22:45 +00:00
|
|
|
// registerOrSignOn tries to sign on, and if that fails (or there are no credentials) tries to register.
|
|
|
|
// Returns an authenticated Flamenco OpenAPI client.
|
|
|
|
func RegisterOrSignOn(ctx context.Context, configWrangler FileConfigWrangler) (
|
2022-02-22 16:13:04 +00:00
|
|
|
client FlamencoClient, startupState api.WorkerStatus,
|
2022-01-31 14:01:51 +00:00
|
|
|
) {
|
|
|
|
// Load configuration
|
2022-03-08 12:51:03 +00:00
|
|
|
cfg, err := configWrangler.WorkerConfig()
|
2022-01-31 14:01:51 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("loading configuration")
|
|
|
|
}
|
|
|
|
|
2022-02-15 09:57:29 +00:00
|
|
|
log.Info().Interface("config", cfg).Msg("loaded configuration")
|
2022-03-08 12:51:03 +00:00
|
|
|
if cfg.ManagerURL == "" {
|
|
|
|
log.Fatal().Msg("no Manager configured")
|
2022-01-31 14:01:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Load credentials
|
2022-03-08 12:51:03 +00:00
|
|
|
creds, err := configWrangler.WorkerCredentials()
|
2022-01-31 14:01:51 +00:00
|
|
|
if err == nil {
|
|
|
|
// Credentials can be loaded just fine, try to sign on with them.
|
|
|
|
client = authenticatedClient(cfg, creds)
|
2022-02-15 10:44:16 +00:00
|
|
|
startupState, err = repeatSignOnUntilAnswer(ctx, cfg, client)
|
2022-01-31 14:01:51 +00:00
|
|
|
if err == nil {
|
|
|
|
// Sign on is fine!
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Either there were no credentials, or existing ones weren't accepted, just register as new worker.
|
2022-02-17 10:22:45 +00:00
|
|
|
client = authenticatedClient(cfg, WorkerCredentials{})
|
2022-01-31 14:01:51 +00:00
|
|
|
creds = register(ctx, cfg, client)
|
|
|
|
|
|
|
|
// store ID and secretKey in config file when registration is complete.
|
2022-03-08 12:51:03 +00:00
|
|
|
err = configWrangler.SaveCredentials(creds)
|
2022-01-31 14:01:51 +00:00
|
|
|
if err != nil {
|
2022-03-08 12:51:03 +00:00
|
|
|
log.Fatal().Err(err).Msg("unable to write credentials file")
|
2022-01-31 14:01:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sign-on should work now.
|
|
|
|
client = authenticatedClient(cfg, creds)
|
|
|
|
startupState, err = signOn(ctx, cfg, client)
|
|
|
|
if err != nil {
|
2022-03-08 12:51:03 +00:00
|
|
|
log.Fatal().Err(err).Str("manager", cfg.ManagerURL).Msg("unable to sign on after registering")
|
2022-01-31 14:01:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// (Re-)register ourselves at the Manager.
|
|
|
|
// Logs a fatal error if unsuccesful.
|
2022-02-22 16:13:04 +00:00
|
|
|
func register(ctx context.Context, cfg WorkerConfig, client FlamencoClient) WorkerCredentials {
|
2022-01-31 14:01:51 +00:00
|
|
|
// Construct our new password.
|
|
|
|
secret := make([]byte, 32)
|
|
|
|
if _, err := rand.Read(secret); err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("unable to generate secret key")
|
|
|
|
}
|
|
|
|
secretKey := hex.EncodeToString(secret)
|
|
|
|
|
|
|
|
req := api.RegisterWorkerJSONRequestBody{
|
2022-06-16 09:02:59 +00:00
|
|
|
Name: mustHostname(),
|
2022-01-31 14:01:51 +00:00
|
|
|
Platform: runtime.GOOS,
|
|
|
|
Secret: secretKey,
|
|
|
|
SupportedTaskTypes: cfg.TaskTypes,
|
|
|
|
}
|
|
|
|
resp, err := client.RegisterWorkerWithResponse(ctx, req)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("error registering at Manager")
|
|
|
|
}
|
|
|
|
switch {
|
|
|
|
case resp.JSON200 != nil:
|
|
|
|
log.Info().
|
|
|
|
Int("code", resp.StatusCode()).
|
|
|
|
Interface("resp", resp.JSON200).
|
|
|
|
Msg("registered at Manager")
|
|
|
|
default:
|
|
|
|
log.Fatal().
|
|
|
|
Int("code", resp.StatusCode()).
|
|
|
|
Interface("resp", resp.JSONDefault).
|
|
|
|
Msg("unable to register at Manager")
|
|
|
|
}
|
|
|
|
|
2022-02-17 10:22:45 +00:00
|
|
|
return WorkerCredentials{
|
2022-01-31 14:01:51 +00:00
|
|
|
WorkerID: resp.JSON200.Uuid,
|
|
|
|
Secret: secretKey,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-15 10:44:16 +00:00
|
|
|
// repeatSignOnUntilAnswer tries to sign on, and only returns when it has been able to reach the Manager.
|
|
|
|
// Return still doesn't mean that the sign-on was succesful; inspect the returned error.
|
2022-02-22 16:13:04 +00:00
|
|
|
func repeatSignOnUntilAnswer(ctx context.Context, cfg WorkerConfig, client FlamencoClient) (api.WorkerStatus, error) {
|
2022-02-15 10:44:16 +00:00
|
|
|
waitTime := 0 * time.Second
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return api.WorkerStatus(""), errSignOnCanceled
|
|
|
|
case <-time.After(waitTime):
|
|
|
|
}
|
|
|
|
|
|
|
|
status, err := signOn(ctx, cfg, client)
|
|
|
|
if err == nil {
|
|
|
|
// Sign-on was succesful, we're done!
|
|
|
|
return status, nil
|
|
|
|
}
|
|
|
|
if err != errSignOnRepeatableFailure {
|
|
|
|
// We shouldn't repeat the sign-on; communication was succesful but somehow our credentials were rejected.
|
|
|
|
return status, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try again after a while.
|
|
|
|
waitTime = 5 * time.Second
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-31 14:01:51 +00:00
|
|
|
// signOn tells the Manager we're alive and returns the status the Manager tells us to go to.
|
2022-02-22 16:13:04 +00:00
|
|
|
func signOn(ctx context.Context, cfg WorkerConfig, client FlamencoClient) (api.WorkerStatus, error) {
|
2022-03-08 12:51:03 +00:00
|
|
|
logger := log.With().Str("manager", cfg.ManagerURL).Logger()
|
2022-01-31 14:01:51 +00:00
|
|
|
|
|
|
|
req := api.SignOnJSONRequestBody{
|
2022-06-16 09:02:59 +00:00
|
|
|
Name: mustHostname(),
|
2022-01-31 14:01:51 +00:00
|
|
|
SupportedTaskTypes: cfg.TaskTypes,
|
2022-02-15 09:57:29 +00:00
|
|
|
SoftwareVersion: appinfo.ApplicationVersion,
|
2022-01-31 14:01:51 +00:00
|
|
|
}
|
2022-02-15 10:44:16 +00:00
|
|
|
|
|
|
|
logger.Info().
|
2022-06-16 09:02:59 +00:00
|
|
|
Str("name", req.Name).
|
2022-02-15 10:44:16 +00:00
|
|
|
Str("softwareVersion", req.SoftwareVersion).
|
|
|
|
Interface("taskTypes", req.SupportedTaskTypes).
|
|
|
|
Msg("signing on at Manager")
|
|
|
|
|
2022-01-31 14:01:51 +00:00
|
|
|
resp, err := client.SignOnWithResponse(ctx, req)
|
|
|
|
if err != nil {
|
|
|
|
logger.Warn().Err(err).Msg("unable to send sign-on request")
|
2022-02-15 10:44:16 +00:00
|
|
|
return "", errSignOnRepeatableFailure
|
2022-01-31 14:01:51 +00:00
|
|
|
}
|
|
|
|
switch {
|
|
|
|
case resp.JSON200 != nil:
|
2022-02-21 18:07:20 +00:00
|
|
|
log.Debug().
|
2022-01-31 14:01:51 +00:00
|
|
|
Int("code", resp.StatusCode()).
|
|
|
|
Interface("resp", resp.JSON200).
|
|
|
|
Msg("signed on at Manager")
|
|
|
|
default:
|
|
|
|
log.Warn().
|
|
|
|
Int("code", resp.StatusCode()).
|
|
|
|
Interface("resp", resp.JSONDefault).
|
|
|
|
Msg("unable to sign on at Manager")
|
2022-02-15 10:44:16 +00:00
|
|
|
return "", errSignOnRejected
|
2022-01-31 14:01:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
startupState := resp.JSON200.StatusRequested
|
|
|
|
log.Info().Str("startup_state", string(startupState)).Msg("manager accepted sign-on")
|
|
|
|
return startupState, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// mustHostname either the hostname or logs a fatal error.
|
|
|
|
func mustHostname() string {
|
2022-06-17 14:24:03 +00:00
|
|
|
name, ok := os.LookupEnv(workerNameEnvVariable)
|
|
|
|
if ok && name != "" {
|
|
|
|
name = strings.TrimSpace(name)
|
|
|
|
log.Info().
|
|
|
|
Str("name", name).
|
|
|
|
Str("fromVariable", workerNameEnvVariable).
|
|
|
|
Msg("worker name obtained from environment variable instead of using the hostname")
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
|
2022-01-31 14:01:51 +00:00
|
|
|
hostname, err := os.Hostname()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("error getting hostname")
|
|
|
|
}
|
|
|
|
return hostname
|
|
|
|
}
|
|
|
|
|
|
|
|
// authenticatedClient constructs a Flamenco client with the given credentials.
|
2022-02-22 16:13:04 +00:00
|
|
|
func authenticatedClient(cfg WorkerConfig, creds WorkerCredentials) FlamencoClient {
|
2022-01-31 14:01:51 +00:00
|
|
|
flamenco, err := api.NewClientWithResponses(
|
2022-03-08 12:51:03 +00:00
|
|
|
cfg.ManagerURL,
|
2022-02-17 12:57:21 +00:00
|
|
|
|
|
|
|
// Add a Basic HTTP authentication header to every request to Flamenco Manager.
|
|
|
|
api.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
|
|
|
|
req.SetBasicAuth(creds.WorkerID, creds.Secret)
|
|
|
|
return nil
|
|
|
|
}),
|
|
|
|
|
|
|
|
// Add a User-Agent header to identify this software + its version.
|
2022-01-31 14:01:51 +00:00
|
|
|
api.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
|
|
|
|
req.Header.Set("User-Agent", appinfo.UserAgent())
|
|
|
|
return nil
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal().Err(err).Msg("error creating client")
|
|
|
|
}
|
|
|
|
|
|
|
|
return flamenco
|
|
|
|
}
|