// 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"); } } } }