// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using EpicGames.Horde.Common;
using EpicGames.Horde.Storage;
using EpicGames.Horde.Storage.Nodes;
using Horde.Build.Agents.Fleet;
using Horde.Build.Agents.Software;
using Horde.Build.Storage.Backends;
using Horde.Build.Utilities;
using Serilog.Events;
using TimeZoneConverter;
namespace Horde.Build
{
///
/// Types of storage backend to use
///
public enum StorageBackendType
{
///
/// Local filesystem
///
FileSystem,
///
/// AWS S3
///
Aws,
///
/// In-memory only (for testing)
///
Memory,
};
///
/// Common settings for different storage backends
///
public interface IStorageBackendOptions : IFileSystemStorageOptions, IAwsStorageOptions
{
///
/// The type of storage backend to use
///
StorageBackendType? Type { get; }
}
///
/// Common settings object for different providers
///
public class StorageBackendOptions : IStorageBackendOptions
{
///
public StorageBackendType? Type { get; set; }
///
public string? BaseDir { get; set; }
///
public string? AwsBucketName { get; set; }
///
public string? AwsBucketPath { get; set; }
///
public AwsCredentialsType AwsCredentials { get; set; }
///
public string? AwsRole { get; set; }
///
public string? AwsProfile { get; set; }
///
public string? AwsRegion { get; set; }
}
///
/// Options for configuring a blob store
///
public class BlobStoreOptions : StorageBackendOptions
{
}
///
/// Options for configuring the default tree store implementation
///
public interface ITreeStoreOptions
{
///
/// Options for creating bundles
///
TreeOptions Bundle { get; }
///
/// Options for chunking content
///
ChunkingOptions Chunking { get; }
}
///
/// Options for storing trees
///
public class TreeStoreOptions : BlobStoreOptions, ITreeStoreOptions
{
///
public TreeOptions Bundle { get; set; } = new TreeOptions();
///
public ChunkingOptions Chunking { get; set; } = new ChunkingOptions();
}
///
/// Authentication method used for logging users in
///
public enum AuthMethod
{
///
/// No authentication enabled, mainly for demo and testing purposes
///
Anonymous,
///
/// OpenID Connect authentication, tailored for Okta
///
Okta,
///
/// Generic OpenID Connect authentication, recommended for most
///
OpenIdConnect,
}
///
/// Type of run mode this process should use. Each carry different types of workloads.
/// More than one mode can be active. But not all modes are not guaranteed to be compatible with each other and will
/// raise an error if combined in such a way.
///
public enum RunMode
{
///
/// Default no-op value (ASP.NET config will default to this for enums that cannot be parsed)
///
None,
///
/// Handle and respond to incoming external requests, such as HTTP REST and gRPC calls.
/// These requests are time-sensitive and short-lived, typically less than 5 secs.
/// If processes handling requests are unavailable, it will be very visible for users.
///
Server,
///
/// Run non-request facing workloads. Such as background services, processing queues, running work
/// based on timers etc. Short periods of downtime or high CPU usage due to bursts are fine for this mode.
/// No user requests will be impacted directly. If auto-scaling is used, a much more aggressive policy can be
/// applied (tighter process packing, higher avg CPU usage).
///
Worker
}
///
/// Feature flags to aid rollout of new features
///
/// Once a feature is running in its intended state and is stable, the flag should be removed.
/// A name and date of when the flag was created is noted next to it to help encourage this behavior.
/// Try having them be just a flag, a boolean.
///
public class FeatureFlagSettings
{
///
/// Use new auth config for custom auth servers in Okta
///
public bool AuthSettingsV2 { get; set; } = false;
///
/// Limit concurrent log chunk writes and await them to reduce mem and I/O usage
///
public bool LimitConcurrentLogChunkWriting { get; set; } = false;
///
/// Whether to use the new log storage backend
///
public bool EnableNewLogger { get; set; } = false;
}
///
/// Options for the commit service
///
public class CommitSettings
{
///
/// Whether to mirror commit metadata to the database
///
public bool ReplicateMetadata { get; set; } = true;
///
/// Whether to mirror commit metadata to the database
///
public bool ReplicateContent { get; set; } = false;
///
/// Options for how objects are packed together
///
public TreeOptions Bundle { get; set; } = new TreeOptions();
///
/// Options for how objects are sliced
///
public ChunkingOptions Chunking { get; set; } = new ChunkingOptions();
}
///
/// Global settings for the application
///
public class ServerSettings
{
///
public RunMode[]? RunModes { get; set; } = null;
///
/// Override the data directory used by Horde. Defaults to C:\ProgramData\HordeServer on Windows, {AppDir}/Data on other platforms.
///
public string? DataDir { get; set; } = null;
///
/// Output level for console
///
public LogEventLevel ConsoleLogLevel { get; set; } = LogEventLevel.Debug;
///
/// Main port for serving HTTP. Uses the default Kestrel port (5000) if not specified.
///
public int HttpPort { get; set; }
///
/// Port for serving HTTP with TLS enabled. Uses the default Kestrel port (5001) if not specified.
///
public int HttpsPort { get; set; }
///
/// Dedicated port for serving only HTTP/2.
///
public int Http2Port { get; set; }
///
/// Whether the server is running as a single instance or with multiple instances, such as in k8s
///
public bool SingleInstance { get; set; } = false;
///
/// MongoDB connection string
///
public string? DatabaseConnectionString { get; set; }
///
/// MongoDB database name
///
public string DatabaseName { get; set; } = "Horde";
///
/// The claim type for administrators
///
public string AdminClaimType { get; set; } = HordeClaimTypes.InternalRole;
///
/// Value of the claim type for administrators
///
public string AdminClaimValue { get; set; } = "admin";
///
/// Optional certificate to trust in order to access the database (eg. AWS public cert for TLS)
///
public string? DatabasePublicCert { get; set; }
///
/// Access the database in read-only mode (avoids creating indices or updating content)
/// Useful for debugging a local instance of HordeServer against a production database.
///
public bool DatabaseReadOnlyMode { get; set; } = false;
///
/// Optional PFX certificate to use for encrypting agent SSL traffic. This can be a self-signed certificate, as long as it's trusted by agents.
///
public string? ServerPrivateCert { get; set; }
///
/// Issuer for tokens from the auth provider
///
public AuthMethod AuthMethod { get; set; } = AuthMethod.Anonymous;
///
/// Issuer for tokens from the auth provider
///
public string? OidcAuthority { get; set; }
///
/// Client id for the OIDC authority
///
public string? OidcClientId { get; set; }
///
/// Client secret for the OIDC authority
///
public string? OidcClientSecret { get; set; }
///
/// Optional redirect url provided to OIDC login
///
public string? OidcSigninRedirect { get; set; }
///
/// OpenID Connect scopes to request when signing in
///
public string[] OidcRequestedScopes { get; set; } = { "profile", "email", "openid" };
///
/// List of fields in /userinfo endpoint to try map to the standard name claim (see System.Security.Claims.ClaimTypes.Name)
///
public string[] OidcClaimNameMapping { get; set; } = { "preferred_username", "email" };
///
/// List of fields in /userinfo endpoint to try map to the standard email claim (see System.Security.Claims.ClaimTypes.Email)
///
public string[] OidcClaimEmailMapping { get; set; } = { "email" };
///
/// List of fields in /userinfo endpoint to try map to the Horde user claim (see HordeClaimTypes.User)
///
public string[] OidcClaimHordeUserMapping { get; set; } = { "preferred_username", "email" };
///
/// List of fields in /userinfo endpoint to try map to the Horde Perforce user claim (see HordeClaimTypes.PerforceUser)
///
public string[] OidcClaimHordePerforceUserMapping { get; set; } = { "preferred_username", "email" };
///
/// Name of the issuer in bearer tokens from the server
///
public string? JwtIssuer { get; set; } = null!;
///
/// Secret key used to sign JWTs. This setting is typically only used for development. In prod, a unique secret key will be generated and stored in the DB for each unique server instance.
///
public string? JwtSecret { get; set; } = null!;
///
/// Length of time before JWT tokens expire, in hours
///
public int JwtExpiryTimeHours { get; set; } = 4;
///
/// Whether to enable Cors, generally for development purposes
///
public bool CorsEnabled { get; set; } = false;
///
/// Allowed Cors origin
///
public string CorsOrigin { get; set; } = null!;
///
/// Whether to enable a schedule in test data (false by default for development builds)
///
public bool EnableScheduleInTestData { get; set; }
///
/// Interval between rebuilding the schedule queue with a DB query.
///
public TimeSpan SchedulePollingInterval { get; set; } = TimeSpan.FromSeconds(60.0);
///
/// Interval between polling for new jobs
///
public TimeSpan NoResourceBackOffTime { get; set; } = TimeSpan.FromSeconds(30.0);
///
/// Interval between attempting to assign agents to take on jobs
///
public TimeSpan InitiateJobBackOffTime { get; set; } = TimeSpan.FromSeconds(180.0);
///
/// Interval between scheduling jobs when an unknown error occurs
///
public TimeSpan UnknownErrorBackOffTime { get; set; } = TimeSpan.FromSeconds(120.0);
///
/// Config for connecting to Redis server(s).
/// Setting it to null will disable Redis use and connection
/// See format at https://stackexchange.github.io/StackExchange.Redis/Configuration.html
///
public string? RedisConnectionConfig { get; set; }
///
/// Type of write cache to use in log service
/// Currently Supported: "InMemory" or "Redis"
///
public string LogServiceWriteCacheType { get; set; } = "InMemory";
///
/// Settings for artifact storage
///
public StorageBackendOptions LogStorage { get; set; } = new StorageBackendOptions() { BaseDir = "Logs" };
///
/// Settings for artifact storage
///
public StorageBackendOptions ArtifactStorage { get; set; } = new StorageBackendOptions() { BaseDir = "Artifacts" };
///
/// Configuration of tree storage
///
public TreeStoreOptions CommitStorage { get; set; } = new TreeStoreOptions() { BaseDir = "Commits" };
///
/// Whether to log json to stdout
///
public bool LogJsonToStdOut { get; set; } = false;
///
/// Whether to log requests to the UpdateSession and QueryServerState RPC endpoints
///
public bool LogSessionRequests { get; set; } = false;
///
/// Default fleet manager to use (when not specified by pool)
///
public FleetManagerType FleetManagerV2 { get; set; } = FleetManagerType.NoOp;
///
/// Config for the fleet manager (serialized JSON)
///
public string? FleetManagerV2Config { get; set; }
///
/// Whether to run scheduled jobs.
///
public bool DisableSchedules { get; set; }
///
/// Timezone for evaluating schedules
///
public string? ScheduleTimeZone { get; set; }
///
/// Token for interacting with Slack
///
public string? SlackToken { get; set; }
///
/// Token for opening a socket to slack
///
public string? SlackSocketToken { get; set; }
///
/// Filtered list of slack users to send notifications to. Should be Slack user ids, separated by commas.
///
public string? SlackUsers { get; set; }
///
/// Prefix to use when reporting errors
///
public string SlackErrorPrefix { get; set; } = ":horde-error: ";
///
/// Prefix to use when reporting warnings
///
public string SlackWarningPrefix { get; set; } = ":horde-warning: ";
///
/// Channel to send stream notification update failures to
///
public string? UpdateStreamsNotificationChannel { get; set; }
///
/// Slack channel to send job related notifications to. Multiple channels can be specified, separated by ;
///
public string? JobNotificationChannel { get; set; }
///
/// URI to the SmtpServer to use for sending email notifications
///
public string? SmtpServer { get; set; }
///
/// The email address to send email notifications from
///
public string? EmailSenderAddress { get; set; }
///
/// The name for the sender when sending email notifications
///
public string? EmailSenderName { get; set; }
///
/// The URl to use for generating links back to the dashboard.
///
public Uri DashboardUrl { get; set; } = new Uri("https://localhost:3000");
///
/// Help email address that users can contact with issues
///
public string? HelpEmailAddress { get; set; }
///
/// Help slack channel that users can use for issues
///
public string? HelpSlackChannel { get; set; }
///
/// The p4 bridge server
///
public string? P4BridgeServer { get; set; }
///
/// The p4 bridge service username
///
public string? P4BridgeServiceUsername { get; set; }
///
/// The p4 bridge service password
///
public string? P4BridgeServicePassword { get; set; }
///
/// Whether the p4 bridge service account can impersonate other users
///
public bool P4BridgeCanImpersonate { get; set; } = false;
///
/// Url of P4 Swarm installation
///
public Uri? P4SwarmUrl { get; set; }
///
/// The Jira service account user name
///
public string? JiraUsername { get; set; }
///
/// The Jira service account API token
///
public string? JiraApiToken { get; set; }
///
/// The Uri for the Jira installation
///
public Uri? JiraUrl { get; set; }
///
/// The number of days shared device checkouts are held
///
public int SharedDeviceCheckoutDays { get; set; } = 3;
///
/// Default agent pool sizing strategy for pools that doesn't have one explicitly configured
///
public PoolSizeStrategy DefaultAgentPoolSizeStrategy { get; set; } = PoolSizeStrategy.LeaseUtilization;
///
/// Scale-out cooldown for auto-scaling agent pools (in seconds). Can be overridden by per-pool settings.
///
public int AgentPoolScaleOutCooldownSeconds { get; set; } = 60; // 1 min
///
/// Scale-in cooldown for auto-scaling agent pools (in seconds). Can be overridden by per-pool settings.
///
public int AgentPoolScaleInCooldownSeconds { get; set; } = 1200; // 20 mins
///
/// Set the minimum size of the global thread pool
/// This value has been found in need of tweaking to avoid timeouts with the Redis client during bursts
/// of traffic. Default is 16 for .NET Core CLR. The correct value is dependent on the traffic the Horde Server
/// is receiving. For Epic's internal deployment, this is set to 40.
///
public int? GlobalThreadPoolMinSize { get; set; }
///
/// Whether to enable Datadog integration for tracing
///
public bool WithDatadog { get; set; }
///
/// Whether to enable Amazon Web Services (AWS) specific features
///
public bool WithAws { get; set; } = false;
///
/// Path to the root config file
///
public string ConfigPath { get; set; } = "Defaults/globals.json";
///
/// Settings for the storage service
///
public StorageOptions? Storage { get; set; }
///
/// Whether to use the local Perforce environment
///
public bool UseLocalPerforceEnv { get; set; }
///
/// Number of pooled perforce connections to keep
///
public int PerforceConnectionPoolSize { get; set; } = 5;
///
/// Whether to enable the upgrade task source.
///
public bool EnableUpgradeTasks { get; set; } = true;
///
/// Whether to enable the conform task source.
///
public bool EnableConformTasks { get; set; } = true;
///
/// Forces configuration data to be read and updated as part of appplication startup, rather than on a schedule. Useful when running locally.
///
public bool ForceConfigUpdateOnStartup { get; set; }
///
/// Whether to open a browser on startup
///
public bool OpenBrowser { get; set; } = false;
///
public FeatureFlagSettings FeatureFlags { get; set; } = new ();
///
/// Options for the commit service
///
public CommitSettings Commits { get; set; } = new CommitSettings();
///
/// Helper method to check if this process has activated the given mode
///
/// Run mode
/// True if mode is active
public bool IsRunModeActive(RunMode mode)
{
if (RunModes == null)
{
return true;
}
return RunModes.Contains(mode);
}
///
/// Validate the settings object does not contain any invalid fields
///
///
public void Validate()
{
if (RunModes != null && IsRunModeActive(RunMode.None))
{
throw new ArgumentException($"Settings key '{nameof(RunModes)}' contains one or more invalid entries");
}
}
}
}