├── .gitattributes ├── nuspecs ├── Dhaf.Core.readme.md └── Dhaf.Core.nuspec ├── project_identity ├── icon.png ├── github.png └── small.pdf ├── src ├── Dhaf.Switchers.Exec │ ├── extension.json │ ├── InternalConfig.cs │ ├── Config.cs │ ├── Dhaf.Switchers.Exec.csproj │ └── ExecSwitcher.cs ├── Dhaf.HealthCheckers.Exec │ ├── extension.json │ ├── InternalConfig.cs │ ├── Config.cs │ ├── Dhaf.HealthCheckers.Exec.csproj │ └── ExecHealthChecker.cs ├── Dhaf.Switchers.GoogleCloud │ ├── extension.json │ ├── InternalConfig.cs │ ├── Config.cs │ ├── Dhaf.Switchers.GoogleCloud.csproj │ └── GoogleCloudSwitcher.cs ├── Dhaf.Core │ ├── Interfaces │ │ ├── Config │ │ │ ├── ISwitcherConfig.cs │ │ │ ├── IHealthCheckerConfig.cs │ │ │ ├── INotifierInternalConfig.cs │ │ │ ├── ISwitcherInternalConfig.cs │ │ │ ├── IHealthCheckerInternalConfig.cs │ │ │ └── INotifierConfig.cs │ │ ├── INotifier │ │ │ ├── Enums │ │ │ │ ├── NotifierRole.cs │ │ │ │ ├── NotifierLevel.cs │ │ │ │ └── NotifierEvent.cs │ │ │ ├── INotifier.cs │ │ │ ├── INotifierEventData.cs │ │ │ ├── NotifierPushOptions.cs │ │ │ ├── NotifierInitOptions.cs │ │ │ └── NotifierEventData.cs │ │ ├── IExtensionConfig.cs │ │ ├── IExtensionInitOptions.cs │ │ ├── IHealthChecker │ │ │ ├── HealthStatus.cs │ │ │ ├── HealthCheckerCheckOptions.cs │ │ │ ├── IHealthChecker.cs │ │ │ └── HealthCheckerInitOptions.cs │ │ ├── ISwitcher │ │ │ ├── SwitcherSwitchOptions.cs │ │ │ ├── ISwitcher.cs │ │ │ └── SwitcherInitOptions.cs │ │ ├── IExtension.cs │ │ └── IExtensionStorageProvider.cs │ ├── Enums │ │ └── DhafNodeRole.cs │ ├── ClusterConfig │ │ ├── Parts │ │ │ ├── SwitcherDefaultConfig.cs │ │ │ ├── HealthCheckerDefaultConfig.cs │ │ │ ├── ClusterEtcdConfig.cs │ │ │ ├── NotifierDefaultConfig.cs │ │ │ ├── ClusterServiceEntryPoint.cs │ │ │ ├── ClusterServiceConfig.cs │ │ │ └── ClusterDhafConfig.cs │ │ ├── ClusterConfig.cs │ │ └── YamlResolving.cs │ ├── Shell │ │ ├── ExecResults.cs │ │ └── Shell.cs │ ├── Exceptions │ │ ├── ConfigParsingException.cs │ │ ├── SwitchFailedException.cs │ │ └── ExtensionInitFailedException.cs │ ├── Dhaf.Core.csproj │ ├── ExtensionsScope │ │ ├── ExtensionLoadContext.cs │ │ └── ExtensionsScope.cs │ └── DhafInternalConfig.cs ├── Dhaf.CLI │ ├── Interfaces │ │ └── IConfigPath.cs │ ├── Definitions.cs │ ├── VerbOptions │ │ ├── StatusDhafOptions.cs │ │ ├── StatusServicesOptions.cs │ │ ├── NodeDecommissionOptions.cs │ │ ├── StatusServiceOptions.cs │ │ ├── SwitchoverPurgeOptions.cs │ │ ├── SwitchoverCandidatesOptions.cs │ │ └── SwitchoverToOptions.cs │ ├── Actions │ │ ├── SwitchoverTo.cs │ │ ├── SwitchoverPurge.cs │ │ ├── NodeDecommission.cs │ │ ├── SwitchoverCandidates.cs │ │ ├── StatusDhaf.cs │ │ ├── ActionBase.cs │ │ ├── StatusService.cs │ │ └── StatusServices.cs │ ├── appsettings.json │ ├── Program.cs │ └── Dhaf.CLI.csproj ├── Dhaf.Switchers.Cloudflare │ ├── extension.json │ ├── DataTransferObjects │ │ ├── ErrorDto.cs │ │ ├── ZoneDto.cs │ │ ├── DnsRecordDto.cs │ │ └── ApiResultDtos.cs │ ├── InternalConfig.cs │ ├── Config.cs │ └── Dhaf.Switchers.Cloudflare.csproj ├── Dhaf.Node.DataTransferObjects │ ├── Node │ │ ├── DhafNodeStatusRaw.cs │ │ ├── UpdatePanicModeRelevanceResult.cs │ │ ├── DhafNodeStatus.cs │ │ ├── EtcdServiceHealth.cs │ │ ├── EtcdManualSwitching.cs │ │ ├── EntryPointStatus.cs │ │ ├── DecisionOfEntryPointSwitching.cs │ │ ├── DemotionStatus.cs │ │ └── PromotionStatus.cs │ ├── RestApi │ │ ├── RestApiError.cs │ │ ├── SwitchoverCandidate.cs │ │ ├── DhafStatus.cs │ │ ├── RestApiResponse.cs │ │ └── ServiceStatus.cs │ └── Dhaf.Node.DataTransferObjects.csproj ├── Dhaf.Notifiers.Telegram │ ├── extension.json │ ├── Config.cs │ ├── InternalConfig.cs │ ├── Dhaf.Notifiers.Telegram.csproj │ ├── StorageActions.cs │ └── UpdatesProcessing.cs ├── Dhaf.Notifiers.Email │ ├── extension.json │ ├── DataTransferObjects │ │ └── MessageData.cs │ ├── InternalConfig.cs │ ├── Config.cs │ └── Dhaf.Notifiers.Email.csproj ├── Dhaf.HealthCheckers.Tcp │ ├── extension.json │ ├── Config.cs │ ├── InternalConfig.cs │ ├── Dhaf.HealthCheckers.Tcp.csproj │ └── TcpHealthChecker.cs ├── Dhaf.Node │ ├── ArgsOptions.cs │ ├── RestApi │ │ ├── RestApiException.cs │ │ ├── RestApiFactory.cs │ │ └── RestApiController.cs │ ├── appsettings.json │ ├── DhafService.cs │ ├── nlog.config │ ├── Dhaf.Node.csproj │ ├── ExtensionStorageProvider.cs │ └── Program.cs ├── Dhaf.HealthCheckers.Web │ ├── extension.json │ ├── Config.cs │ ├── DownReasonResolver.cs │ ├── InternalConfig.cs │ └── Dhaf.HealthCheckers.Web.csproj └── Dhaf.sln ├── templates ├── agents │ └── google-cloud │ │ ├── services.yaml │ │ ├── nginx │ │ ├── dhaf_serv1_ip │ │ └── nginx_template.conf │ │ ├── systemd │ │ └── gc-nginx-agent.service │ │ ├── gc-nginx-agent.py │ │ └── README.md ├── dhaf_extensions │ ├── Dhaf.Templates.Extensions.FooSwitcher │ │ ├── extension.json │ │ ├── Config.cs │ │ ├── InternalConfig.cs │ │ ├── Dhaf.Templates.Extensions.FooSwitcher.csproj │ │ └── FooSwitcher.cs │ ├── Dhaf.Templates.Extensions.FooHealthChecker │ │ ├── extension.json │ │ ├── Config.cs │ │ ├── InternalConfig.cs │ │ ├── Dhaf.Templates.Extensions.FooHealthChecker.csproj │ │ └── FooHealthChecker.cs │ └── Dhaf.Templates.Extensions.sln ├── systemd │ └── README.md └── build_example.sh ├── tests └── Dhaf.Core.Tests │ ├── Data │ ├── test_config_issue_25.dhaf │ └── test_config_1.dhaf │ ├── Mocks │ └── ExtensionConfigsMock.cs │ ├── appsettings.json │ ├── Dhaf.Core.Tests.csproj │ └── ClusterConfigParserTest.cs ├── .github ├── _README.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── dhaf-core-nuget.yml │ ├── dhaf-release.yml │ ├── osx-x64.yml │ ├── win-x64.yml │ └── linux-x64.yml ├── LICENSE └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /nuspecs/Dhaf.Core.readme.md: -------------------------------------------------------------------------------- 1 | # Dhaf.Core 2 | The core package for developing **dhaf** extensions. -------------------------------------------------------------------------------- /project_identity/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperion-cs/dhaf/HEAD/project_identity/icon.png -------------------------------------------------------------------------------- /project_identity/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperion-cs/dhaf/HEAD/project_identity/github.png -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Exec/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.Switchers.Exec.dll", 3 | "internalConfiguration": {} 4 | } -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Exec/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.HealthCheckers.Exec.dll", 3 | "internalConfiguration": {} 4 | } -------------------------------------------------------------------------------- /src/Dhaf.Switchers.GoogleCloud/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.Switchers.GoogleCloud.dll", 3 | "internalConfiguration": {} 4 | } -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/Config/ISwitcherConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public interface ISwitcherConfig : IExtensionConfig { } 4 | } 5 | -------------------------------------------------------------------------------- /templates/agents/google-cloud/services.yaml: -------------------------------------------------------------------------------- 1 | - name: serv1 2 | gcmd_key: dhaf_serv1_ip 3 | nginx_subconfig_path: /etc/nginx/dhaf/dhaf_serv1_ip 4 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/Config/IHealthCheckerConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public interface IHealthCheckerConfig : IExtensionConfig { } 4 | } 5 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/Config/INotifierInternalConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public interface INotifierInternalConfig : IExtensionConfig { } 4 | } 5 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/Config/ISwitcherInternalConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public interface ISwitcherInternalConfig : IExtensionConfig { } 4 | } 5 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Interfaces/IConfigPath.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.CLI 2 | { 3 | public interface IConfigPath 4 | { 5 | public string Config { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Enums/DhafNodeRole.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public enum DhafNodeRole 4 | { 5 | Leader = 1, Follower = 2, Candidate = 3 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/Config/IHealthCheckerInternalConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public interface IHealthCheckerInternalConfig : IExtensionConfig { } 4 | } 5 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/INotifier/Enums/NotifierRole.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public enum NotifierRole 4 | { 5 | Leader, Follower 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Definitions.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.CLI 2 | { 3 | static class Definitions 4 | { 5 | public const string APPLICATION_ALIAS = "dhaf.cli"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/INotifier/Enums/NotifierLevel.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public enum NotifierLevel 4 | { 5 | Info, Warning, Critical 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooSwitcher/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.Templates.Extensions.FooSwitcher.dll", 3 | "internalConfiguration": {} 4 | } -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/IExtensionConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public interface IExtensionConfig 4 | { 5 | public string ExtensionName { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooHealthChecker/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.Templates.Extensions.FooHealthChecker.dll", 3 | "internalConfiguration": {} 4 | } -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/Config/INotifierConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public interface INotifierConfig : IExtensionConfig 4 | { 5 | string Name { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/IExtensionInitOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public interface IExtensionInitOptions 4 | { 5 | IExtensionStorageProvider Storage { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Cloudflare/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.Switchers.Cloudflare.dll", 3 | "internalConfiguration": { 4 | "baseUrl": "https://api.cloudflare.com/client/v4/" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Node/DhafNodeStatusRaw.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class DhafNodeStatusRaw 4 | { 5 | public long LastHeartbeatTimestamp { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /templates/agents/google-cloud/nginx/dhaf_serv1_ip: -------------------------------------------------------------------------------- 1 | # If necessary, it will be overridden automatically via agent. 2 | # Typically, this file is located in /etc/nginx/dhaf/dhaf_serv1_ip 3 | 4 | set $dhaf_ip "100.10.10.10"; -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Telegram/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.Notifiers.Telegram.dll", 3 | "internalConfiguration": { 4 | "updatesPollingInterval": 2, 5 | "storageSubscribersPath": "subs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Node/UpdatePanicModeRelevanceResult.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class UpdatePanicModeRelevanceResult 4 | { 5 | public bool HasStatusChanged { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/RestApi/RestApiError.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class RestApiError 4 | { 5 | public int Code { get; set; } 6 | public string Message { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Email/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.Notifiers.Email.dll", 3 | "internalConfiguration": { 4 | "senderName": "Dhaf Notifier", 5 | "securitySslFlag": "ssl", 6 | "timeout": 10000 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/IHealthChecker/HealthStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public class HealthStatus 4 | { 5 | public bool Healthy { get; set; } 6 | public int? ReasonCode { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Tcp/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.HealthCheckers.Tcp.dll", 3 | "internalConfiguration": { 4 | "defReceiveTimeout": 5, 5 | "minReceiveTimeout": 1, 6 | "maxReceiveTimeout": 86400 7 | } 8 | } -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Node/DhafNodeStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class DhafNodeStatus 4 | { 5 | public string Name { get; set; } 6 | public bool Healthy { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/RestApi/SwitchoverCandidate.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class SwitchoverCandidate 4 | { 5 | public string Name { get; set; } 6 | public int Priority { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Email/DataTransferObjects/MessageData.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Notifiers.Email 2 | { 3 | public class MessageData 4 | { 5 | public string Subject { get; set; } 6 | public string Body { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Cloudflare/DataTransferObjects/ErrorDto.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Switchers.Cloudflare 2 | { 3 | public class ErrorDto 4 | { 5 | public int Code { get; set; } 6 | public string Message { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/ISwitcher/SwitcherSwitchOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public class SwitcherSwitchOptions 4 | { 5 | public string EntryPointId { get; set; } 6 | public bool Failover { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Exec/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Switchers.Exec 4 | { 5 | public class InternalConfig : ISwitcherInternalConfig 6 | { 7 | public string ExtensionName => "exec"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Exec/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.HealthCheckers.Exec 4 | { 5 | public class InternalConfig : IHealthCheckerInternalConfig 6 | { 7 | public string ExtensionName => "exec"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooSwitcher/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Switchers.Foo 4 | { 5 | public class Config : ISwitcherConfig 6 | { 7 | public string ExtensionName => "foo"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/IHealthChecker/HealthCheckerCheckOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public class HealthCheckerCheckOptions 4 | { 5 | /// Entry point ID. 6 | public string EntryPointId { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.GoogleCloud/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Switchers.GoogleCloud 4 | { 5 | public class InternalConfig : ISwitcherInternalConfig 6 | { 7 | public string ExtensionName => "google-cloud"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooHealthChecker/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.HealthCheckers.Foo 4 | { 5 | public class Config : IHealthCheckerConfig 6 | { 7 | public string ExtensionName => "foo"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Dhaf.Node/ArgsOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | 3 | namespace Dhaf.Node 4 | { 5 | public class ArgsOptions 6 | { 7 | [Option('c', "config", Required = true, HelpText = "Configuration file.")] 8 | public string ConfigPath { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/INotifier/INotifier.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public interface INotifier : IExtension 6 | { 7 | Task Init(NotifierInitOptions options); 8 | Task Push(NotifierPushOptions options); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Node/EtcdServiceHealth.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class EtcdServiceHealth 4 | { 5 | public bool Healthy { get; set; } 6 | public long Timestamp { get; set; } 7 | public int? ReasonCode { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Cloudflare/DataTransferObjects/ZoneDto.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Switchers.Cloudflare 2 | { 3 | public class ZoneDto 4 | { 5 | public string Id { get; set; } 6 | public string Status { get; set; } 7 | public bool Paused { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/INotifier/INotifierEventData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public interface INotifierEventData 6 | { 7 | string Service { get; } 8 | string DhafCluster { get; } 9 | DateTime UtcTimestamp { get; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/RestApi/DhafStatus.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Dhaf.Node 4 | { 5 | public class DhafStatus 6 | { 7 | public string Leader { get; set; } 8 | public IEnumerable Nodes { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Cloudflare/DataTransferObjects/DnsRecordDto.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Switchers.Cloudflare 2 | { 3 | public class DnsRecordDto 4 | { 5 | public string Id { get; set; } 6 | public string Content { get; set; } 7 | public bool Proxied { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Cloudflare/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | namespace Dhaf.Switchers.Cloudflare 3 | { 4 | public class InternalConfig : ISwitcherInternalConfig 5 | { 6 | public string ExtensionName => "cloudflare"; 7 | public string BaseUrl { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ClusterConfig/Parts/SwitcherDefaultConfig.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class SwitcherDefaultConfig : ISwitcherConfig 6 | { 7 | [YamlMember(Alias = "type")] 8 | public string ExtensionName { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Node/EtcdManualSwitching.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class EtcdManualSwitching 4 | { 5 | public string DhafNode { get; set; } 6 | 7 | /// Entry point ID. 8 | public string EpId { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Exec/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Switchers.Exec 4 | { 5 | public class Config : ISwitcherConfig 6 | { 7 | public string ExtensionName => "exec"; 8 | 9 | public string Init { get; set; } 10 | public string Switch { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /templates/agents/google-cloud/systemd/gc-nginx-agent.service: -------------------------------------------------------------------------------- 1 | Description=Google Cloud nginx agent 2 | After=network.target 3 | 4 | [Service] 5 | Type=simple 6 | WorkingDirectory=/opt/dhaf/ 7 | ExecStart=/usr/bin/python3 /opt/dhaf/gc-nginx-agent.py 8 | Restart=on-failure 9 | 10 | [Install] 11 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /src/Dhaf.Core/ClusterConfig/Parts/HealthCheckerDefaultConfig.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class HealthCheckerDefaultConfig : IHealthCheckerConfig 6 | { 7 | [YamlMember(Alias = "type")] 8 | public string ExtensionName { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Shell/ExecResults.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public class ExecResults 4 | { 5 | public string Output { get; set; } 6 | public double TotalExecuteTime { get; set; } 7 | public int ExitCode { get; set; } 8 | 9 | public bool Success { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/INotifier/NotifierPushOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public class NotifierPushOptions 4 | { 5 | public NotifierLevel Level { get; set; } 6 | 7 | public NotifierEvent Event { get; set; } 8 | public INotifierEventData EventData { get; set; } 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Exec/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.HealthCheckers.Exec 4 | { 5 | public class Config : IHealthCheckerConfig 6 | { 7 | public string ExtensionName => "exec"; 8 | 9 | public string Init { get; set; } 10 | public string Check { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Tcp/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.HealthCheckers.Tcp 4 | { 5 | public class Config : IHealthCheckerConfig 6 | { 7 | public string ExtensionName => "tcp"; 8 | 9 | public int? Port { get; set; } 10 | public int? ReceiveTimeout { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Cloudflare/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Switchers.Cloudflare 4 | { 5 | public class Config : ISwitcherConfig 6 | { 7 | public string ExtensionName => "cloudflare"; 8 | public string ApiToken { get; set; } 9 | public string Zone { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/Dhaf.Core.Tests/Data/test_config_issue_25.dhaf: -------------------------------------------------------------------------------- 1 | dhaf: 2 | cluster-name: test-cr 3 | node-name: node-1 4 | 5 | services: 6 | - name: serv1 7 | domain: site.com 8 | entry-points: 9 | - name: nc1 10 | ip: 100.1.1.1 11 | switcher: 12 | type: cloudflare 13 | health-checker: 14 | type: web 15 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ClusterConfig/Parts/ClusterEtcdConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public class ClusterEtcdConfig 4 | { 5 | public string Hosts { get; set; } 6 | 7 | public string Username { get; set; } 8 | public string Password { get; set; } 9 | 10 | public int? LeaderKeyTtl { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/ISwitcher/ISwitcher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public interface ISwitcher : IExtension 6 | { 7 | Task Init(SwitcherInitOptions options); 8 | Task Switch(SwitcherSwitchOptions options); 9 | 10 | Task GetCurrentEntryPointId(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ClusterConfig/Parts/NotifierDefaultConfig.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class NotifierDefaultConfig : INotifierConfig 6 | { 7 | [YamlMember(Alias = "type")] 8 | public string ExtensionName { get; set; } 9 | public string Name { get => "aa"; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Node/EntryPointStatus.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Dhaf.Node 4 | { 5 | public class EntryPointStatus 6 | { 7 | public string EntryPointId { get; set; } 8 | public bool Healthy { get; set; } 9 | 10 | public IEnumerable Reasons { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Telegram/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Notifiers.Telegram 4 | { 5 | public class Config : INotifierConfig 6 | { 7 | public string ExtensionName => "tg"; 8 | public string Name { get; set; } 9 | public string JoinCode { get; set; } 10 | public string Token { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Telegram/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Notifiers.Telegram 4 | { 5 | public class InternalConfig : INotifierInternalConfig 6 | { 7 | public string ExtensionName => "tg"; 8 | public int UpdatesPollingInterval { get; set; } 9 | public string StorageSubscribersPath { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ClusterConfig/Parts/ClusterServiceEntryPoint.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class ClusterServiceEntryPoint 6 | { 7 | [YamlMember(Alias = "name")] 8 | public string Id { get; set; } 9 | 10 | [YamlMember(Alias = "ip")] 11 | public string IP { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Dhaf.Node.DataTransferObjects.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Email/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Notifiers.Email 4 | { 5 | public class InternalConfig : INotifierInternalConfig 6 | { 7 | public string ExtensionName => "email"; 8 | public string SenderName { get; set; } 9 | public string SecuritySslFlag { get; set; } 10 | public int Timeout { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/IHealthChecker/IHealthChecker.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public interface IHealthChecker : IExtension 6 | { 7 | Task Init(HealthCheckerInitOptions config); 8 | Task Check(HealthCheckerCheckOptions options); 9 | 10 | Task ResolveUnhealthinessReasonCode(int code); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.GoogleCloud/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Switchers.GoogleCloud 4 | { 5 | public class Config : ISwitcherConfig 6 | { 7 | public string ExtensionName => "google-cloud"; 8 | 9 | public string Project { get; set; } 10 | public string MetadataKey { get; set; } 11 | public string CredentialsPath { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/IExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Dhaf.Core 5 | { 6 | public interface IExtension 7 | { 8 | string ExtensionName { get; } 9 | string Sign { get; } 10 | Type ConfigType { get; } 11 | Type InternalConfigType { get; } 12 | 13 | Task DhafNodeRoleChangedEventHandler(DhafNodeRole role); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Node/DecisionOfEntryPointSwitching.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class DecisionOfEntryPointSwitching 4 | { 5 | public bool IsRequired { get; set; } 6 | public bool Failover { get; set; } 7 | 8 | /// 9 | /// Host ID for switching. 10 | /// 11 | public string SwitchTo { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Dhaf.Node/RestApi/RestApiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dhaf.Node 4 | { 5 | public class RestApiException : Exception 6 | { 7 | public int Code { get; } 8 | public override string Message { get; } 9 | 10 | public RestApiException(int code, string message) 11 | { 12 | Code = code; 13 | Message = message; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Tcp/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.HealthCheckers.Tcp 4 | { 5 | public class InternalConfig : IHealthCheckerInternalConfig 6 | { 7 | public string ExtensionName => "tcp"; 8 | 9 | public int DefReceiveTimeout { get; set; } 10 | public int MinReceiveTimeout { get; set; } 11 | public int MaxReceiveTimeout { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Node/DemotionStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class DemotionStatus 4 | { 5 | /// Flag the success of the demotion of the current node to cluster follower. 6 | public bool Success { get; set; } 7 | 8 | /// The node name of the current cluster leader. 9 | public string Leader { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Exceptions/ConfigParsingException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class ConfigParsingException : Exception 6 | { 7 | public int Code { get; } 8 | public override string Message { get; } 9 | 10 | public ConfigParsingException(int code, string message) 11 | { 12 | Code = code; 13 | Message = message; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/_README.md: -------------------------------------------------------------------------------- 1 | Tags serve as the trigger for actions. 2 | Actions (tag assignment) must be running in a way that ensures the release occurs last, e.g.: 3 | ``` 4 | vX.Y.Z-core-nuget 5 | vX.Y.Z-linux-x64 6 | vX.Y.Z-osx-x64 7 | vX.Y.Z-win-x64 8 | vX.Y.Z // github release tag 9 | ``` 10 | 11 | If a tag is deleted on the origin, it will remain locally (which might cause issues). To remove it, use: 12 | ```sh 13 | git tag -d 14 | ``` 15 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooSwitcher/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Switchers.Foo 4 | { 5 | // The dhaf extension system will automatically populate an 6 | // instance of this class from the internalConfiguration member of the extension.json file. 7 | public class InternalConfig : ISwitcherInternalConfig 8 | { 9 | public string ExtensionName => "foo"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ClusterConfig/ClusterConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class ClusterConfig 6 | { 7 | public ClusterDhafConfig Dhaf { get; set; } = new(); 8 | public ClusterEtcdConfig Etcd { get; set; } = new(); 9 | public List Services { get; set; } = new(); 10 | public List Notifiers { get; set; } = new(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/RestApi/RestApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Dhaf.Node 4 | { 5 | public class RestApiResponse 6 | { 7 | public bool Success { get; set; } 8 | 9 | public IEnumerable Errors { get; set; } = new List(); 10 | } 11 | 12 | public class RestApiResponse : RestApiResponse 13 | { 14 | public T Data { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooHealthChecker/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.HealthCheckers.Foo 4 | { 5 | // The dhaf extension system will automatically populate an 6 | // instance of this class from the internalConfiguration member of the extension.json file. 7 | public class InternalConfig : IHealthCheckerInternalConfig 8 | { 9 | public string ExtensionName => "foo"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/INotifier/NotifierInitOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class NotifierInitOptions : IExtensionInitOptions 6 | { 7 | public ILogger Logger { get; set; } 8 | public INotifierConfig Config { get; set; } 9 | public INotifierInternalConfig InternalConfig { get; set; } 10 | public IExtensionStorageProvider Storage { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /templates/agents/google-cloud/nginx/nginx_template.conf: -------------------------------------------------------------------------------- 1 | server { 2 | # You can add your own changes to the configuration. The main thing is to have reverse proxying to $dhaf_ip. 3 | 4 | server_name site.com; 5 | listen 80; 6 | 7 | include "dhaf/dhaf_serv1_ip"; # Sets $dhaf_ip for the current virtual nginx server. 8 | location / { 9 | proxy_pass http://$dhaf_ip; 10 | } 11 | } 12 | 13 | # ... You can add several virtual servers in the same way. -------------------------------------------------------------------------------- /src/Dhaf.Core/Exceptions/SwitchFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class SwitchFailedException : Exception 6 | { 7 | public string ExtensionSign { get; } 8 | public override string Message { get => $"Failed to switch with {ExtensionSign}. See log for details."; } 9 | 10 | public SwitchFailedException(string extensionSign) 11 | { 12 | ExtensionSign = extensionSign; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Exceptions/ExtensionInitFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class ExtensionInitFailedException : Exception 6 | { 7 | public string ExtensionSign { get; } 8 | public override string Message { get => $"Failed to initialize extension {ExtensionSign}. See log for details."; } 9 | 10 | public ExtensionInitFailedException(string extensionSign) 11 | { 12 | ExtensionSign = extensionSign; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/IExtensionStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace Dhaf.Core 5 | { 6 | public interface IExtensionStorageProvider 7 | { 8 | Task PutAsync(string key, string value); 9 | 10 | Task DeleteAsync(string key); 11 | Task DeleteRangeAsync(string keyPrefix); 12 | 13 | Task GetAsyncOfDefault(string key); 14 | Task> GetRangeAsync(string keyPrefix); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ClusterConfig/Parts/ClusterServiceConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using YamlDotNet.Serialization; 3 | 4 | namespace Dhaf.Core 5 | { 6 | public class ClusterServiceConfig 7 | { 8 | public string Name { get; set; } 9 | public string Domain { get; set; } 10 | 11 | public List EntryPoints { get; set; } = new(); 12 | public ISwitcherConfig Switcher { get; set; } 13 | public IHealthCheckerConfig HealthChecker { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Dhaf.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/ISwitcher/SwitcherInitOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class SwitcherInitOptions : IExtensionInitOptions 6 | { 7 | public ILogger Logger { get; set; } 8 | public ISwitcherConfig Config { get; set; } 9 | public ISwitcherInternalConfig InternalConfig { get; set; } 10 | public ClusterServiceConfig ClusterServiceConfig { get; set; } 11 | public IExtensionStorageProvider Storage { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/IHealthChecker/HealthCheckerInitOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public class HealthCheckerInitOptions : IExtensionInitOptions 6 | { 7 | public ILogger Logger { get; set; } 8 | public IHealthCheckerConfig Config { get; set; } 9 | public IHealthCheckerInternalConfig InternalConfig { get; set; } 10 | public ClusterServiceConfig ClusterServiceConfig { get; set; } 11 | public IExtensionStorageProvider Storage { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/INotifier/Enums/NotifierEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public enum NotifierEvent 4 | { 5 | // Note: "Ep" is "Entry point". 6 | 7 | EpDown, EpUp, // EpHealthChanged 8 | Failover, Switchover, Switching, // CurrentEpChanged 9 | SwitchoverPurged, // SwitchoverPurged 10 | ServiceUp, ServiceDown, // ServiceHealthChanged 11 | DhafNodeUp, DhafNodeDown, // DhafNodeHealthChanged 12 | DhafNewLeader // DhafNewLeader 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/Node/PromotionStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Node 2 | { 3 | public class PromotionStatus 4 | { 5 | /// Flag the success of the promotion of the current node to cluster leader. 6 | public bool Success { get; set; } 7 | 8 | /// The node name of the current cluster leader. 9 | public string Leader { get; set; } 10 | 11 | /// If it was successful to become a leader, it contains the lease id of the leader's key. 12 | public long? LeaderLeaseId { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Dhaf.Node.DataTransferObjects/RestApi/ServiceStatus.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Dhaf.Node 4 | { 5 | public class ServiceStatus 6 | { 7 | public string Name { get; set; } 8 | public string Domain { get; set; } 9 | public string CurrentEntryPointName { get; set; } 10 | public IEnumerable EntryPoints { get; set; } 11 | public string SwitchoverRequirement { get; set; } 12 | } 13 | 14 | public class ServiceEntryPointStatus 15 | { 16 | public string Name { get; set; } 17 | public bool Healthy { get; set; } 18 | public int Priority { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Email/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | 3 | namespace Dhaf.Notifiers.Email 4 | { 5 | public class Config : INotifierConfig 6 | { 7 | public string ExtensionName => "email"; 8 | public string Name { get; set; } 9 | public SmtpConfig Smtp { get; set; } 10 | 11 | public string From { get; set; } 12 | public string To { get; set; } 13 | } 14 | 15 | public class SmtpConfig 16 | { 17 | public string Server { get; set; } 18 | public int Port { get; set; } 19 | public string Security { get; set; } 20 | public string Username { get; set; } 21 | public string Password { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ClusterConfig/Parts/ClusterDhafConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core 2 | { 3 | public class ClusterDhafConfig 4 | { 5 | public string ClusterName { get; set; } 6 | public string NodeName { get; set; } 7 | 8 | public int? HealthyNodeStatusTtl { get; set; } 9 | public int? HeartbeatInterval { get; set; } 10 | public int? TactInterval { get; set; } 11 | public int? TactPostSwitchDelay { get; set; } 12 | 13 | public ClusterDhafWebApiConfig WebApi { get; set; } = new(); 14 | } 15 | 16 | public class ClusterDhafWebApiConfig 17 | { 18 | public string Host { get; set; } 19 | public int? Port { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Web/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoint": "Dhaf.HealthCheckers.Web.dll", 3 | "internalConfiguration": { 4 | "defHttpPort": 80, 5 | "defHttpsPort": 443, 6 | "httpSchema": "http", 7 | "httpsSchema": "https", 8 | 9 | "defMethod": "GET", 10 | "defPath": "", 11 | "defHeaders": {}, 12 | "defFollowRedirects": false, 13 | "defTimeout": 5, 14 | "defRetries": 2, 15 | "defExpectedCodes": "200", 16 | "expectedCodesSeparator": ",", 17 | "expectedCodesWildcard": "X", 18 | "defExpectedResponseBody": "", 19 | 20 | "minTimeout": 1, 21 | "maxTimeout": 86400, 22 | 23 | "defDomainForwarding": true, 24 | "defIgnoreSslErrors": false, 25 | 26 | "hostHeader": "Host" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooSwitcher/Dhaf.Templates.Extensions.FooSwitcher.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net7.0 4 | linux-x64;osx-x64;win-x64 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooHealthChecker/Dhaf.Templates.Extensions.FooHealthChecker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net7.0 4 | linux-x64;osx-x64;win-x64 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/dhaf-core-nuget.yml: -------------------------------------------------------------------------------- 1 | name: Dhaf.Core Nuget (Dev) 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*-core-nuget' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: setup .net 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 9.0.x 18 | 19 | - name: prepare nuget package 20 | run: dotnet pack src/Dhaf.Core/Dhaf.Core.csproj --configuration Release -p:NuspecFile=../../nuspecs/Dhaf.Core.nuspec --output nupkgs 21 | 22 | - name: get the package file path 23 | run: echo "PKG_FILE=$(ls nupkgs | head -n 1)" >> $GITHUB_ENV 24 | 25 | - name: push package 26 | run: dotnet nuget push "nupkgs/${{ env.PKG_FILE }}" --api-key "${{ secrets.NUGET_TOKEN }}" --source https://api.nuget.org/v3/index.json -------------------------------------------------------------------------------- /src/Dhaf.CLI/VerbOptions/StatusDhafOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using CommandLine.Text; 3 | using System.Collections.Generic; 4 | 5 | namespace Dhaf.CLI 6 | { 7 | [Verb("status-dhaf", HelpText = "Show dhaf cluster status information.")] 8 | public class StatusDhafOptions : IConfigPath 9 | { 10 | [Option('c', "config", Required = true, HelpText = "Configuration file.")] 11 | public string Config { get; set; } 12 | 13 | [Usage(ApplicationAlias = Definitions.APPLICATION_ALIAS)] 14 | public static IEnumerable Examples 15 | { 16 | get 17 | { 18 | yield return new Example("Show dhaf cluster status information using configuration file ", 19 | new StatusDhafOptions { Config = "" }); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '...' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Code/Config** 23 | ``` 24 | If applicable, add code to help explain your problem. 25 | ``` 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Stand (please complete the following information):** 31 | - OS: [e.g. Ubuntu 20.04] 32 | - Dhaf version: [e.g. 1.0.0] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/VerbOptions/StatusServicesOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using CommandLine.Text; 3 | using System.Collections.Generic; 4 | 5 | namespace Dhaf.CLI 6 | { 7 | [Verb("status-services", HelpText = "Show all services status information.")] 8 | public class StatusServicesOptions : IConfigPath 9 | { 10 | [Option('c', "config", Required = true, HelpText = "Configuration file.")] 11 | public string Config { get; set; } 12 | 13 | [Usage(ApplicationAlias = Definitions.APPLICATION_ALIAS)] 14 | public static IEnumerable Examples 15 | { 16 | get 17 | { 18 | yield return new Example("Show all services status information using configuration file ", 19 | new StatusServiceOptions { Config = "" }); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Web/Config.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using System.Collections.Generic; 3 | 4 | namespace Dhaf.HealthCheckers.Web 5 | { 6 | public class Config : IHealthCheckerConfig 7 | { 8 | public string ExtensionName => "web"; 9 | public string Schema { get; set; } 10 | public string Method { get; set; } 11 | public int? Port { get; set; } 12 | public string Path { get; set; } 13 | public Dictionary Headers { get; set; } 14 | public bool? FollowRedirects { get; set; } 15 | public int? Timeout { get; set; } 16 | public int? Retries { get; set; } 17 | public string ExpectedCodes { get; set; } 18 | public string ExpectedResponseBody { get; set; } 19 | public bool? DomainForwarding { get; set; } 20 | public bool? IgnoreSslErrors { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Actions/SwitchoverTo.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Node; 2 | using RestSharp; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace Dhaf.CLI 7 | { 8 | public static partial class Actions 9 | { 10 | public static async Task ExecuteSwitchoverToAndReturnExitCode(SwitchoverToOptions opt) 11 | { 12 | await PrepareRestClient(opt); 13 | var request = new RestRequest($"switchover?entryPointId={opt.EpName}&serviceName={opt.ServiceName}"); 14 | 15 | var response = await _restClient.GetAsync(request); 16 | if (!response.Success) 17 | { 18 | PrintErrors(response.Errors); 19 | return -1; 20 | } 21 | 22 | Console.WriteLine($"OK. A request for a switchover to has been sent."); 23 | 24 | return 0; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Actions/SwitchoverPurge.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Node; 2 | using RestSharp; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dhaf.CLI 8 | { 9 | public static partial class Actions 10 | { 11 | public static async Task ExecuteSwitchoverPurgeAndReturnExitCode(SwitchoverPurgeOptions opt) 12 | { 13 | await PrepareRestClient(opt); 14 | var request = new RestRequest($"switchover/purge?serviceName={opt.ServiceName}"); 15 | 16 | var response = await _restClient.GetAsync(request); 17 | if (!response.Success) 18 | { 19 | PrintErrors(response.Errors); 20 | return -1; 21 | } 22 | 23 | Console.WriteLine("OK. The switchover requirement has been purged."); 24 | 25 | return 0; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Actions/NodeDecommission.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Node; 2 | using RestSharp; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dhaf.CLI 8 | { 9 | public static partial class Actions 10 | { 11 | public static async Task ExecuteNodeDecommissionAndReturnExitCode(NodeDecommissionOptions opt) 12 | { 13 | await PrepareRestClient(opt); 14 | var request = new RestRequest($"dhaf/node/decommission?name={opt.NodeName}"); 15 | 16 | var response = await _restClient.GetAsync(request); 17 | if (!response.Success) 18 | { 19 | PrintErrors(response.Errors); 20 | return -1; 21 | } 22 | 23 | Console.WriteLine($"OK. The dhaf node has been successfully decommissioned."); 24 | 25 | return 0; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /templates/systemd/README.md: -------------------------------------------------------------------------------- 1 | # Systemd Template 2 | 3 | 1. Create systemd service file (it's standard path for the most Linux distros, but you should check it before): 4 | ```shell 5 | nano /etc/systemd/system/dhaf.service 6 | 7 | ``` 8 | 2. Edit this basic service (especially paths and params): 9 | ```shell 10 | [Unit] 11 | Description=Dhaf 12 | After=network.target 13 | 14 | [Service] 15 | Type=simple 16 | WorkingDirectory=/opt/dhaf 17 | ExecStart=/opt/dhaf/dhaf.node -c config-n1.dhaf 18 | Restart=on-failure 19 | 20 | [Install] 21 | WantedBy=multi-user.target 22 | 23 | ``` 24 | 3. Reload daemons: 25 | ```shell 26 | systemctl daemon-reload 27 | 28 | ``` 29 | 4. Test dhaf service: 30 | ```shell 31 | systemctl restart dhaf.service 32 | # Check status, it should be active 33 | systemctl status dhaf.service 34 | 35 | ``` 36 | 5. Enable it, to autostart service after reboot: 37 | ```shell 38 | systemctl enable dhaf.service 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/VerbOptions/NodeDecommissionOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using CommandLine.Text; 3 | using System.Collections.Generic; 4 | 5 | namespace Dhaf.CLI 6 | { 7 | [Verb("node-decommission", HelpText = "Decommission the dhaf node.")] 8 | public class NodeDecommissionOptions : IConfigPath 9 | { 10 | [Option('c', "config", Required = true, HelpText = "Configuration file.")] 11 | public string Config { get; set; } 12 | 13 | [Value(0, Required = true, HelpText = "Node name.")] 14 | public string NodeName { get; set; } 15 | 16 | 17 | [Usage(ApplicationAlias = Definitions.APPLICATION_ALIAS)] 18 | public static IEnumerable Examples 19 | { 20 | get 21 | { 22 | yield return new Example("Decommission the dhaf node using configuration file ", 23 | new NodeDecommissionOptions { Config = "", NodeName = "n1" }); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /nuspecs/Dhaf.Core.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dhaf.Core 5 | 1.2.5 6 | The core package for developing dhaf extensions. 7 | Petr Osetrov 8 | dhaf core extension cluster high availability fault tolerance 9 | 10 | false 11 | MIT 12 | Copyright © 2024 Petr Osetrov 13 | docs/Dhaf.Core.readme.md 14 | images\icon.png 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Cloudflare/DataTransferObjects/ApiResultDtos.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Dhaf.Switchers.Cloudflare 5 | { 6 | public abstract class ResultBaseDto where T : class 7 | { 8 | public bool Success { get; set; } 9 | public List Errors { get; set; } 10 | 11 | public string PrettyErrors(string action) 12 | { 13 | var separator = "\n"; 14 | var intro = $"Errors occurred ({Errors.Count} pcs.) in the action \"{action}\":"; 15 | var errors = string.Join(separator, Errors.Select(x => $"Error {x.Code}: {x.Message}")); 16 | 17 | return $"{intro}\n{errors}"; 18 | } 19 | } 20 | 21 | public class ResultDto : ResultBaseDto where T : class 22 | { 23 | public T Result { get; set; } 24 | } 25 | 26 | public class ResultCollectionDto : ResultBaseDto where T : class 27 | { 28 | public List Result { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Dhaf.Core.Tests/Mocks/ExtensionConfigsMock.cs: -------------------------------------------------------------------------------- 1 | namespace Dhaf.Core.Tests 2 | { 3 | public class SwitcherConfigMock : ISwitcherConfig 4 | { 5 | public string ExtensionName => "a"; 6 | } 7 | 8 | public class SwitcherInternalConfigMock : ISwitcherInternalConfig 9 | { 10 | public string ExtensionName => "a"; 11 | } 12 | 13 | public class HealthCheckerConfigMock : IHealthCheckerConfig 14 | { 15 | public string ExtensionName => "a"; 16 | } 17 | 18 | public class HealthCheckerInternalConfigMock : IHealthCheckerInternalConfig 19 | { 20 | public string ExtensionName => "a"; 21 | } 22 | 23 | public class NotifierConfigMock : INotifierConfig 24 | { 25 | public string ExtensionName => "a"; 26 | 27 | public string Name => "a"; 28 | } 29 | public class NotifierInternalConfigMock : INotifierInternalConfig 30 | { 31 | public string ExtensionName => "a"; 32 | 33 | public string DefName => "a"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/VerbOptions/StatusServiceOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using CommandLine.Text; 3 | using System.Collections.Generic; 4 | 5 | namespace Dhaf.CLI 6 | { 7 | [Verb("status-service", HelpText = "Show service status information.")] 8 | public class StatusServiceOptions : IConfigPath 9 | { 10 | [Option('c', "config", Required = true, HelpText = "Configuration file.")] 11 | public string Config { get; set; } 12 | 13 | [Option('s', "service", Required = true, HelpText = "Service name.")] 14 | public string ServiceName { get; set; } 15 | 16 | [Usage(ApplicationAlias = Definitions.APPLICATION_ALIAS)] 17 | public static IEnumerable Examples 18 | { 19 | get 20 | { 21 | yield return new Example("Show service status information using configuration file ", 22 | new StatusServiceOptions { Config = "", ServiceName = "" }); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/VerbOptions/SwitchoverPurgeOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using CommandLine.Text; 3 | using System.Collections.Generic; 4 | 5 | namespace Dhaf.CLI 6 | { 7 | [Verb("switchover-purge", HelpText = "Purge the switchover requirement.")] 8 | public class SwitchoverPurgeOptions : IConfigPath 9 | { 10 | [Option('c', "config", Required = true, HelpText = "Configuration file.")] 11 | public string Config { get; set; } 12 | 13 | [Option('s', "service", Required = true, HelpText = "Service name.")] 14 | public string ServiceName { get; set; } 15 | 16 | [Usage(ApplicationAlias = Definitions.APPLICATION_ALIAS)] 17 | public static IEnumerable Examples 18 | { 19 | get 20 | { 21 | yield return new Example("Purge the switchover requirement in service using configuration file ", 22 | new SwitchoverPurgeOptions { Config = "", ServiceName = "" }); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Dhaf.Core.Tests/Data/test_config_1.dhaf: -------------------------------------------------------------------------------- 1 | dhaf: 2 | cluster-name: test-cr 3 | node-name: node-1 4 | web-api: 5 | host: localhost 6 | port: 8128 7 | 8 | etcd: 9 | hosts: http://11.22.33.44:2379 10 | 11 | services: 12 | - name: serv1 13 | domain: site.com 14 | entry-points: 15 | - name: nc1 16 | ip: 100.1.1.1 17 | - name: nc2 18 | ip: 100.1.1.2 19 | - name: nc3 20 | ip: 100.1.1.3 21 | switcher: 22 | type: cloudflare 23 | api-token: aaa 24 | zone: site.com 25 | health-checker: 26 | type: web 27 | schema: http 28 | 29 | - name: serv2 30 | domain: foo.site.com 31 | entry-points: 32 | - name: nc1 33 | ip: 120.1.1.1 34 | - name: nc2 35 | ip: 120.1.1.2 36 | switcher: 37 | type: cloudflare 38 | api-token: bbb 39 | zone: site.com 40 | health-checker: 41 | type: web 42 | schema: http 43 | 44 | notifiers: 45 | - type: tg 46 | token: tg-token-aaa 47 | join-code: w2r6KPSgf2SnD6yM 48 | name: ntf-tg-1 49 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/VerbOptions/SwitchoverCandidatesOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using CommandLine.Text; 3 | using System.Collections.Generic; 4 | 5 | namespace Dhaf.CLI 6 | { 7 | [Verb("switchover-candidates", HelpText = "Show suitable entry points for switchover.")] 8 | public class SwitchoverCandidatesOptions : IConfigPath 9 | { 10 | [Option('c', "config", Required = true, HelpText = "Configuration file.")] 11 | public string Config { get; set; } 12 | 13 | [Option('s', "service", Required = true, HelpText = "Service name.")] 14 | public string ServiceName { get; set; } 15 | 16 | [Usage(ApplicationAlias = Definitions.APPLICATION_ALIAS)] 17 | public static IEnumerable Examples 18 | { 19 | get 20 | { 21 | yield return new Example("Show suitable entry points in service for switchover using configuration file ", 22 | new SwitchoverCandidatesOptions { Config = "", ServiceName = "" }); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Shell/Shell.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Dhaf.Core 4 | { 5 | public static class Shell 6 | { 7 | public static ExecResults Exec(string fileName, string args = "") 8 | { 9 | var psi = new ProcessStartInfo(fileName, args) 10 | { 11 | RedirectStandardOutput = true, 12 | }; 13 | 14 | var proc = Process.Start(psi); 15 | if (proc == null) 16 | { 17 | return new ExecResults { Success = false }; 18 | } 19 | 20 | var output = proc.StandardOutput 21 | .ReadToEnd() 22 | .TrimEnd('\r', '\n'); 23 | 24 | proc.WaitForExit(); 25 | 26 | var totalExecTime = (proc.ExitTime - proc.StartTime).TotalMilliseconds; 27 | 28 | 29 | return new ExecResults 30 | { 31 | Success = true, 32 | ExitCode = proc.ExitCode, 33 | TotalExecuteTime = totalExecTime, 34 | Output = output 35 | }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Petr Osetrov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "defHeartbeatInterval": 5, 3 | "defTactInterval": 10, 4 | "tactIntervalMin": 1, 5 | "tactIntervalMax": 3600, 6 | "defTactPostSwitchDelay": 0, 7 | "tactPostSwitchDelayMin": 0, 8 | "tactPostSwitchDelayMax": 3600, 9 | "defHealthyNodeStatusTtl": 30, 10 | "nameMaxLength": 64, 11 | "extensions": [ 12 | "health-checkers/web", 13 | "health-checkers/exec", 14 | "health-checkers/tcp", 15 | "switchers/cloudflare", 16 | "switchers/google-cloud", 17 | "switchers/exec", 18 | "notifiers/email", 19 | "notifiers/tg" 20 | ], 21 | "etcd": { 22 | "leaderPath": "leader", 23 | "defLeaderKeyTtl": 15, 24 | "nodesPath": "nodes/", 25 | "healthPath": "health/", 26 | "switchoverPath": "switchover/", 27 | "extensionStoragePath": "extensions", 28 | "extensionStorageHcPrefix": "hc/", 29 | "extensionStorageSwPrefix": "sw/", 30 | "extensionStorageNtfPrefix": "ntf/" 31 | }, 32 | "webApi": { 33 | "DefHost": "localhost", 34 | "DefPort": 8128 35 | }, 36 | "Logging": { 37 | "LogLevel": { 38 | "Default": "Trace", 39 | "Microsoft": "Warning" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Dhaf.Node/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "defHeartbeatInterval": 5, 3 | "defTactInterval": 10, 4 | "tactIntervalMin": 1, 5 | "tactIntervalMax": 3600, 6 | "defTactPostSwitchDelay": 0, 7 | "tactPostSwitchDelayMin": 0, 8 | "tactPostSwitchDelayMax": 3600, 9 | "defHealthyNodeStatusTtl": 30, 10 | "nameMaxLength": 64, 11 | "extensions": [ 12 | "health-checkers/web", 13 | "health-checkers/exec", 14 | "health-checkers/tcp", 15 | "switchers/cloudflare", 16 | "switchers/google-cloud", 17 | "switchers/exec", 18 | "notifiers/email", 19 | "notifiers/tg" 20 | ], 21 | "etcd": { 22 | "leaderPath": "leader", 23 | "defLeaderKeyTtl": 15, 24 | "nodesPath": "nodes/", 25 | "healthPath": "health/", 26 | "switchoverPath": "switchover/", 27 | "extensionStoragePath": "extensions", 28 | "extensionStorageHcPrefix": "hc/", 29 | "extensionStorageSwPrefix": "sw/", 30 | "extensionStorageNtfPrefix": "ntf/" 31 | }, 32 | "webApi": { 33 | "DefHost": "localhost", 34 | "DefPort": 8128 35 | }, 36 | "Logging": { 37 | "LogLevel": { 38 | "Default": "Trace", 39 | "Microsoft": "Warning" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Exec/Dhaf.Switchers.Exec.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | true 9 | 10 | 11 | 12 | 13 | false 14 | runtime 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/Dhaf.Core.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "defHeartbeatInterval": 5, 3 | "defTactInterval": 10, 4 | "tactIntervalMin": 1, 5 | "tactIntervalMax": 3600, 6 | "defTactPostSwitchDelay": 0, 7 | "tactPostSwitchDelayMin": 0, 8 | "tactPostSwitchDelayMax": 3600, 9 | "defHealthyNodeStatusTtl": 30, 10 | "nameMaxLength": 64, 11 | "extensions": [ 12 | "health-checkers/web", 13 | "health-checkers/exec", 14 | "health-checkers/tcp", 15 | "switchers/cloudflare", 16 | "switchers/google-cloud", 17 | "switchers/exec", 18 | "notifiers/email", 19 | "notifiers/tg" 20 | ], 21 | "etcd": { 22 | "leaderPath": "leader", 23 | "defLeaderKeyTtl": 15, 24 | "nodesPath": "nodes/", 25 | "healthPath": "health/", 26 | "switchoverPath": "switchover/", 27 | "extensionStoragePath": "extensions", 28 | "extensionStorageHcPrefix": "hc/", 29 | "extensionStorageSwPrefix": "sw/", 30 | "extensionStorageNtfPrefix": "ntf/" 31 | }, 32 | "webApi": { 33 | "DefHost": "localhost", 34 | "DefPort": 8128 35 | }, 36 | "Logging": { 37 | "LogLevel": { 38 | "Default": "Trace", 39 | "Microsoft": "Warning" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Exec/Dhaf.HealthCheckers.Exec.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | true 9 | 10 | 11 | 12 | 13 | false 14 | runtime 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Tcp/Dhaf.HealthCheckers.Tcp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | true 9 | 10 | 11 | 12 | 13 | false 14 | runtime 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/VerbOptions/SwitchoverToOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using CommandLine.Text; 3 | using System.Collections.Generic; 4 | 5 | namespace Dhaf.CLI 6 | { 7 | [Verb("switchover-to", HelpText = "Switchover to the specified entry point.")] 8 | public class SwitchoverToOptions : IConfigPath 9 | { 10 | [Option('c', "config", Required = true, HelpText = "Configuration file.")] 11 | public string Config { get; set; } 12 | 13 | [Option('s', "service", Required = true, HelpText = "Service name.")] 14 | public string ServiceName { get; set; } 15 | 16 | [Value(0, Required = true, HelpText = "Entry point name.")] 17 | public string EpName { get; set; } 18 | 19 | [Usage(ApplicationAlias = Definitions.APPLICATION_ALIAS)] 20 | public static IEnumerable Examples 21 | { 22 | get 23 | { 24 | yield return new Example("Switchover to the specified entry point in service using configuration file ", 25 | new SwitchoverToOptions { EpName = "ep-1", Config = "", ServiceName = "" }); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Dhaf.Core/Interfaces/INotifier/NotifierEventData.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace Dhaf.NotifierEventData 6 | { 7 | public class Base : INotifierEventData 8 | { 9 | public DateTime UtcTimestamp { get; set; } 10 | 11 | public string DhafCluster { get; set; } 12 | public string Service { get; set; } 13 | } 14 | 15 | // Note: "Ep" is "Entry point". 16 | 17 | public class EpHealthChanged : Base 18 | { 19 | public string EpName { get; set; } 20 | public IEnumerable Reasons { get; set; } 21 | } 22 | 23 | public class CurrentEpChanged : Base 24 | { 25 | public string FromEp { get; set; } 26 | public string ToEp { get; set; } 27 | } 28 | 29 | public class SwitchoverPurged : Base 30 | { 31 | public string SwitchoverEp { get; set; } 32 | } 33 | 34 | public class DhafNewLeader : Base 35 | { 36 | public string Leader { get; set; } 37 | } 38 | 39 | public class DhafNodeHealthChanged : Base 40 | { 41 | public string NodeName { get; set; } 42 | } 43 | 44 | public class ServiceHealthChanged : Base { } 45 | } 46 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Email/Dhaf.Notifiers.Email.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | false 18 | runtime 19 | 20 | 21 | 22 | 23 | 24 | Always 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Web/DownReasonResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Dhaf.HealthCheckers.Web 4 | { 5 | public enum DownReason 6 | { 7 | Timeout = 0, 8 | UnexpectedHttpCode = 1, 9 | UnexpectedResponseBody = 2, 10 | NotCompleted = 3, 11 | SslPolicyErrors = 4, 12 | NetworkOrHttpFrameworkException = 5 13 | } 14 | 15 | public static class DownReasonResolver 16 | { 17 | public static string Resolve(int code) 18 | { 19 | var x = (DownReason)code; 20 | 21 | if (!Map.ContainsKey(x)) 22 | { 23 | return "Unexpected reason"; 24 | } 25 | 26 | return Map[x]; 27 | } 28 | 29 | private static Dictionary Map { get; set; } = new() 30 | { 31 | { DownReason.Timeout, "HTTP timeout occurred" }, 32 | { DownReason.UnexpectedHttpCode, "Unexpected http code" }, 33 | { DownReason.UnexpectedResponseBody, "Unexpected response body" }, 34 | { DownReason.NotCompleted, "Not completed" }, 35 | { DownReason.SslPolicyErrors, "Ssl policy errors" }, 36 | { DownReason.NetworkOrHttpFrameworkException, "Network or http framework exception" } 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.GoogleCloud/Dhaf.Switchers.GoogleCloud.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | false 18 | runtime 19 | 20 | 21 | 22 | 23 | 24 | Always 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Web/InternalConfig.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using System.Collections.Generic; 3 | 4 | namespace Dhaf.HealthCheckers.Web 5 | { 6 | public class InternalConfig : IHealthCheckerInternalConfig 7 | { 8 | public string ExtensionName => "web"; 9 | public int DefHttpPort { get; set; } 10 | public int DefHttpsPort { get; set; } 11 | public string HttpSchema { get; set; } 12 | public string HttpsSchema { get; set; } 13 | 14 | public string DefMethod { get; set; } 15 | public string DefPath { get; set; } 16 | public Dictionary DefHeaders { get; set; } 17 | public bool DefFollowRedirects { get; set; } 18 | public int DefTimeout { get; set; } 19 | public int DefRetries { get; set; } 20 | 21 | public string DefExpectedCodes { get; set; } 22 | public string ExpectedCodesSeparator { get; set; } 23 | public string ExpectedCodesWildcard { get; set; } 24 | 25 | public string DefExpectedResponseBody { get; set; } 26 | 27 | public int MinTimeout { get; set; } 28 | public int MaxTimeout { get; set; } 29 | 30 | public bool DefDomainForwarding { get; set; } 31 | public bool DefIgnoreSslErrors { get; set; } 32 | 33 | public string HostHeader { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Telegram/Dhaf.Notifiers.Telegram.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | false 19 | runtime 20 | 21 | 22 | 23 | 24 | 25 | Always 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Program.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using System.Threading.Tasks; 3 | 4 | namespace Dhaf.CLI 5 | { 6 | class Program 7 | { 8 | static async Task Main(string[] args) 9 | { 10 | await Parser.Default.ParseArguments(args) 14 | .MapResult 15 | ( 16 | (StatusDhafOptions opts) => Actions.ExecuteStatusDhafAndReturnExitCode(opts), 17 | (StatusServiceOptions opts) => Actions.ExecuteStatusServiceAndReturnExitCode(opts), 18 | (StatusServicesOptions opts) => Actions.ExecuteStatusServicesAndReturnExitCode(opts), 19 | (SwitchoverCandidatesOptions opts) => Actions.ExecuteSwitchoverCandidatesAndReturnExitCode(opts), 20 | (SwitchoverToOptions opts) => Actions.ExecuteSwitchoverToAndReturnExitCode(opts), 21 | (SwitchoverPurgeOptions opts) => Actions.ExecuteSwitchoverPurgeAndReturnExitCode(opts), 22 | (NodeDecommissionOptions opts) => Actions.ExecuteNodeDecommissionAndReturnExitCode(opts), 23 | errs => Task.FromResult(0) 24 | ); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Cloudflare/Dhaf.Switchers.Cloudflare.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | false 19 | runtime 20 | 21 | 22 | 23 | 24 | 25 | Always 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Dhaf.CLI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | linux-x64;osx-x64;win-x64 7 | true 8 | 1.2.5 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Telegram/StorageActions.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dhaf.Notifiers.Telegram 8 | { 9 | public partial class TelegramNotifier : INotifier 10 | { 11 | protected async Task PutSubscriber(long id, string type) 12 | { 13 | var key = $"{_internalConfig.StorageSubscribersPath}/{id}"; 14 | await _storage.PutAsync(key, type); 15 | } 16 | 17 | protected async Task DeleteSubscriber(long id) 18 | { 19 | var key = $"{_internalConfig.StorageSubscribersPath}/{id}"; 20 | await _storage.DeleteAsync(key); 21 | } 22 | 23 | protected async Task GetSubscriberOfDefault(long id) 24 | { 25 | var key = $"{_internalConfig.StorageSubscribersPath}/{id}"; 26 | var value = await _storage.GetAsyncOfDefault(key); 27 | 28 | if (value is null) 29 | { 30 | return null; 31 | } 32 | 33 | return id; 34 | } 35 | 36 | protected async Task> GetSubscribers() 37 | { 38 | var subs = await _storage.GetRangeAsync(_internalConfig.StorageSubscribersPath); 39 | 40 | var subIds = subs.Select(x => long.Parse(Path.GetFileName(x.Key))); 41 | return subIds; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Web/Dhaf.HealthCheckers.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | linux-x64;osx-x64;win-x64 6 | true 7 | 1.2.5 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | false 20 | runtime 21 | 22 | 23 | 24 | 25 | 26 | Always 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Dhaf.Node/DhafService.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using System.Collections.Generic; 3 | 4 | namespace Dhaf.Node 5 | { 6 | public class DhafService 7 | { 8 | public string Name { get; set; } 9 | public string Domain { get; set; } 10 | public List EntryPoints { get; set; } 11 | public ISwitcher Switcher { get; set; } 12 | public IHealthChecker HealthChecker { get; set; } 13 | 14 | /// 15 | /// The flag shows that all entry points is unhealthy. 16 | /// The service is DOWN and dhaf is physically unable to fix the situation on its own. 17 | /// 18 | public bool PanicMode { get; set; } = false; 19 | 20 | public string SwitchoverLastRequirementInStorage { get; set; } = null; 21 | 22 | public string CurrentEntryPointId { get; set; } 23 | 24 | public IEnumerable EntryPointStatuses { get; set; } 25 | public IEnumerable PreviousEntryPointStatuses { get; set; } // For tracking changes. 26 | 27 | public DhafService(string name, string domain, List entryPoints, 28 | ISwitcher switcher, IHealthChecker healthChecker) 29 | { 30 | Name = name; 31 | Domain = domain; 32 | EntryPoints = entryPoints; 33 | Switcher = switcher; 34 | HealthChecker = healthChecker; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Dhaf.Node/nlog.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Actions/SwitchoverCandidates.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Node; 2 | using RestSharp; 3 | using Spectre.Console; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Dhaf.CLI 10 | { 11 | public static partial class Actions 12 | { 13 | public static async Task ExecuteSwitchoverCandidatesAndReturnExitCode(SwitchoverCandidatesOptions opt) 14 | { 15 | await PrepareRestClient(opt); 16 | var request = new RestRequest($"switchover/candidates?serviceName={opt.ServiceName}"); 17 | 18 | var response = await _restClient.GetAsync>>(request); 19 | if (!response.Success) 20 | { 21 | PrintErrors(response.Errors); 22 | return -1; 23 | } 24 | 25 | var table = new Table(); 26 | table.Border(TableBorder.Ascii2); 27 | table.Width = 50; 28 | table.Title = new TableTitle("Switchover candidates"); 29 | table.AddColumns("Priority", "Name"); 30 | 31 | table.Columns[0].Centered(); 32 | table.Columns[1].Centered(); 33 | 34 | var candidates = response.Data; 35 | foreach (var c in candidates) 36 | { 37 | table.AddRow(c.Priority.ToString(), c.Name); 38 | } 39 | 40 | AnsiConsole.Write(table); 41 | return 0; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Actions/StatusDhaf.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Node; 2 | using RestSharp; 3 | using Spectre.Console; 4 | using System.Threading.Tasks; 5 | 6 | namespace Dhaf.CLI 7 | { 8 | public static partial class Actions 9 | { 10 | public static async Task ExecuteStatusDhafAndReturnExitCode(StatusDhafOptions opt) 11 | { 12 | await PrepareRestClient(opt); 13 | var request = new RestRequest($"dhaf/status"); 14 | 15 | var response = await _restClient.GetAsync>(request); 16 | if (!response.Success) 17 | { 18 | PrintErrors(response.Errors); 19 | return -1; 20 | } 21 | 22 | var dhafStatus = response.Data; 23 | 24 | var table = new Table(); 25 | table.Border(TableBorder.Ascii2); 26 | table.Width = 50; 27 | table.Title = new TableTitle("Dhaf cluster status"); 28 | table.AddColumns("Node name", "Healthy", "Role"); 29 | 30 | table.Columns[0].Centered(); 31 | table.Columns[1].Centered(); 32 | table.Columns[2].Centered(); 33 | 34 | foreach (var node in dhafStatus.Nodes) 35 | { 36 | table.AddRow(node.Name, 37 | node.Healthy ? "[green]Yes[/]" : "[red]No[/]", 38 | dhafStatus.Leader == node.Name ? "[white]Leader[/]" : "Follower"); 39 | } 40 | 41 | AnsiConsole.Write(table); 42 | return 0; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ExtensionsScope/ExtensionLoadContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.Loader; 4 | 5 | namespace Dhaf.Core 6 | { 7 | class ExtensionLoadContext : AssemblyLoadContext 8 | { 9 | private readonly AssemblyDependencyResolver _resolver; 10 | 11 | public ExtensionLoadContext(string extensionPath) 12 | { 13 | _resolver = new AssemblyDependencyResolver(extensionPath); 14 | } 15 | 16 | protected override Assembly Load(AssemblyName assemblyName) 17 | { 18 | // See more about this check here: 19 | // https://github.com/dotnet/runtime/issues/87578 20 | try 21 | { 22 | var asm = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName); 23 | if (asm != null) 24 | { 25 | return asm; 26 | } 27 | } 28 | catch 29 | { 30 | // Assembly is not part of the host - load it into the plugin. 31 | var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); 32 | if (assemblyPath != null) 33 | { 34 | return LoadFromAssemblyPath(assemblyPath); 35 | } 36 | } 37 | 38 | return null; 39 | } 40 | 41 | protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) 42 | { 43 | var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); 44 | if (libraryPath != null) 45 | { 46 | return LoadUnmanagedDllFromPath(libraryPath); 47 | } 48 | 49 | return IntPtr.Zero; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Dhaf.Node/Dhaf.Node.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | linux-x64;osx-x64;win-x64 7 | true 8 | 1.2.5 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Always 36 | 37 | 38 | Always 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/Dhaf.Core.Tests/Dhaf.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | 6 | false 7 | 1.2.5 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | PreserveNewest 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31702.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Templates.Extensions.FooSwitcher", "Dhaf.Templates.Extensions.FooSwitcher\Dhaf.Templates.Extensions.FooSwitcher.csproj", "{8387F31C-97B0-4903-B858-639358AE84A9}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Templates.Extensions.FooHealthChecker", "Dhaf.Templates.Extensions.FooHealthChecker\Dhaf.Templates.Extensions.FooHealthChecker.csproj", "{59B10033-CCA2-49CE-BCD7-1C9A32D37069}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {8387F31C-97B0-4903-B858-639358AE84A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {8387F31C-97B0-4903-B858-639358AE84A9}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {8387F31C-97B0-4903-B858-639358AE84A9}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {8387F31C-97B0-4903-B858-639358AE84A9}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {59B10033-CCA2-49CE-BCD7-1C9A32D37069}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {59B10033-CCA2-49CE-BCD7-1C9A32D37069}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {59B10033-CCA2-49CE-BCD7-1C9A32D37069}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {59B10033-CCA2-49CE-BCD7-1C9A32D37069}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {69BF9CF9-877A-4DA9-8283-9AC292E23173} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Actions/ActionBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Dhaf.Core; 6 | using Dhaf.Node; 7 | using Microsoft.Extensions.Configuration; 8 | using RestSharp; 9 | using RestSharp.Serializers.NewtonsoftJson; 10 | 11 | namespace Dhaf.CLI 12 | { 13 | public static partial class Actions 14 | { 15 | private static IRestClient _restClient; 16 | 17 | private static async Task GetClusterConfig(IConfigPath opt) 18 | { 19 | var _configuration = GetConfiguration(); 20 | 21 | var internalConfig = new DhafInternalConfig(); 22 | _configuration.Bind(internalConfig); 23 | 24 | var clusterConfigParser = new ClusterConfigParser(opt.Config, internalConfig); 25 | var config = await clusterConfigParser.Parse(); 26 | 27 | return config; 28 | } 29 | 30 | private static async Task PrepareRestClient(IConfigPath opt) 31 | { 32 | var config = await GetClusterConfig(opt); 33 | var webApiEndpoint = config.Dhaf.WebApi; 34 | var uri = new Uri($"http://{webApiEndpoint.Host}:{webApiEndpoint.Port}/"); 35 | 36 | var options = new RestClientOptions { BaseUrl = uri }; 37 | _restClient = new RestClient(options, configureSerialization: s => s.UseNewtonsoftJson()); 38 | } 39 | 40 | private static void PrintErrors(IEnumerable errors) 41 | { 42 | var error = errors.First(); 43 | Console.WriteLine($"Error {error.Code}: {error.Message}"); 44 | } 45 | 46 | private static IConfigurationRoot GetConfiguration() 47 | { 48 | return new ConfigurationBuilder() 49 | .AddJsonFile("appsettings.json") 50 | .Build(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/dhaf-release.yml: -------------------------------------------------------------------------------- 1 | name: Dhaf Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.[0-9]+' 7 | 8 | jobs: 9 | release: 10 | if: startsWith(github.ref, 'refs/tags') 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: download linux-x64 artifacts 15 | uses: dawidd6/action-download-artifact@v6 16 | with: 17 | workflow: linux-x64.yml 18 | workflow_conclusion: success 19 | path: linux-x64 20 | 21 | - name: download win-x64 artifacts 22 | uses: dawidd6/action-download-artifact@v6 23 | with: 24 | workflow: win-x64.yml 25 | workflow_conclusion: success 26 | path: win-x64 27 | 28 | - name: download osx-x64 artifacts 29 | uses: dawidd6/action-download-artifact@v6 30 | with: 31 | workflow: osx-x64.yml 32 | workflow_conclusion: success 33 | path: osx-x64 34 | 35 | - name: set the release version from tag 36 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 37 | 38 | - name: zip linux-x64 artifacts 39 | uses: papeloto/action-zip@v1 40 | with: 41 | files: linux-x64 42 | recursive: true 43 | dest: dhaf-${{ env.RELEASE_VERSION }}-linux-x64.zip 44 | 45 | - name: zip win-x64 artifacts 46 | uses: papeloto/action-zip@v1 47 | with: 48 | files: win-x64 49 | recursive: true 50 | dest: dhaf-${{ env.RELEASE_VERSION }}-win-x64.zip 51 | 52 | - name: zip osx-x64 artifacts 53 | uses: papeloto/action-zip@v1 54 | with: 55 | files: osx-x64 56 | recursive: true 57 | dest: dhaf-${{ env.RELEASE_VERSION }}-osx-x64.zip 58 | 59 | - name: release 60 | uses: softprops/action-gh-release@v1 61 | with: 62 | files: | 63 | dhaf-${{ env.RELEASE_VERSION }}-linux-x64.zip 64 | dhaf-${{ env.RELEASE_VERSION }}-win-x64.zip 65 | dhaf-${{ env.RELEASE_VERSION }}-osx-x64.zip 66 | -------------------------------------------------------------------------------- /src/Dhaf.Core/DhafInternalConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | using System.Text.Json; 4 | 5 | namespace Dhaf.Core 6 | { 7 | public class DhafInternalConfig 8 | { 9 | public DhafInternalConfigEtcd Etcd { get; set; } 10 | public DhafInternalConfigWebApi WebApi { get; set; } 11 | 12 | public int DefHeartbeatInterval { get; set; } 13 | 14 | public int DefTactInterval { get; set; } 15 | public int TactIntervalMin { get; set; } 16 | public int TactIntervalMax { get; set; } 17 | 18 | public int DefTactPostSwitchDelay { get; set; } 19 | public int TactPostSwitchDelayMin { get; set; } 20 | public int TactPostSwitchDelayMax { get; set; } 21 | 22 | public int DefHealthyNodeStatusTtl { get; set; } 23 | 24 | public List Extensions { get; set; } 25 | 26 | public static Encoding ConfigsEncoding { get; set; } = Encoding.UTF8; 27 | 28 | public int NameMaxLength { get; set; } 29 | 30 | public static JsonSerializerOptions JsonSerializerOptions { get; set; } 31 | = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; 32 | } 33 | 34 | public class DhafInternalConfigEtcd 35 | { 36 | public string LeaderPath { get; set; } 37 | public int DefLeaderKeyTtl { get; set; } 38 | 39 | public string NodesPath { get; set; } 40 | public string HealthPath { get; set; } 41 | public string SwitchoverPath { get; set; } 42 | public string ExtensionStoragePath { get; set; } 43 | 44 | public string ExtensionStorageHcPrefix { get; set; } 45 | public string ExtensionStorageSwPrefix { get; set; } 46 | public string ExtensionStorageNtfPrefix { get; set; } 47 | } 48 | 49 | public class DhafInternalConfigWebApi 50 | { 51 | public string DefHost { get; set; } 52 | public int DefPort { get; set; } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /templates/agents/google-cloud/gc-nginx-agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import asyncio 4 | import functools 5 | import logging 6 | import yaml 7 | 8 | with open('services.yaml') as f: 9 | services = yaml.safe_load(f) 10 | 11 | TACT_TIMEOUT = 30 12 | APPLY_CHANGES_COMMAND = "systemctl reload nginx" 13 | EVENT_LOOP = asyncio.get_event_loop() 14 | 15 | logging.basicConfig( 16 | format='%(asctime)s %(levelname)s: %(message)s', level=logging.INFO) 17 | 18 | 19 | def GET_NGINX_SUBCONFIG_CONTENT(ip): return f"set $dhaf_ip \"{str(ip)}\";" 20 | 21 | 22 | async def get_gc_metadata(md_key, timeout=0): 23 | path = f"http://metadata.google.internal/computeMetadata/v1/project/attributes/{md_key}" 24 | 25 | if timeout > 0: 26 | path = path + f"?wait_for_change=true&timeout_sec={TACT_TIMEOUT}" 27 | 28 | request_task = EVENT_LOOP.run_in_executor(None, functools.partial( 29 | requests.get, path, headers={"Metadata-Flavor": "Google"})) 30 | resp = await request_task 31 | 32 | if resp.status_code == 200: 33 | return resp.content.decode('UTF-8') 34 | else: 35 | return None 36 | 37 | 38 | async def watch_gc_metadata(service_name, gcmd_key, nginx_subconfig_path): 39 | prev_value = await get_gc_metadata(gcmd_key) 40 | logging.info(f"Start watch [{service_name}] (init value: {prev_value})") 41 | while True: 42 | curr_value = await get_gc_metadata(gcmd_key, TACT_TIMEOUT) 43 | 44 | if curr_value is not None and curr_value != prev_value: 45 | logging.warning( 46 | f"[{service_name}] Changed the value <{prev_value}> → <{curr_value}>.") 47 | 48 | prev_value = curr_value 49 | curr_content = GET_NGINX_SUBCONFIG_CONTENT(prev_value) 50 | 51 | try: 52 | with open(nginx_subconfig_path, "w") as f: 53 | f.write(curr_content) 54 | 55 | os.system(APPLY_CHANGES_COMMAND) 56 | logging.info(f"[{service_name}] Changes applied.") 57 | 58 | except Exception as e: 59 | logging.error(str(e)) 60 | 61 | if os.geteuid() != 0: 62 | logging.critical("The \"google cloud nginx agent\" must be run as root.") 63 | exit() 64 | 65 | logging.info("The \"google cloud nginx agent\" started...") 66 | tasks = [watch_gc_metadata( 67 | i["name"], i["gcmd_key"], i["nginx_subconfig_path"]) for i in services] 68 | 69 | EVENT_LOOP.run_until_complete(asyncio.wait(tasks)) 70 | EVENT_LOOP.close() 71 | -------------------------------------------------------------------------------- /src/Dhaf.Node/ExtensionStorageProvider.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using dotnet_etcd; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace Dhaf.Node 7 | { 8 | public class ExtensionStorageProvider : IExtensionStorageProvider 9 | { 10 | private readonly EtcdClient _etcdClient; 11 | private readonly Grpc.Core.Metadata _etcdHeaders; 12 | private readonly ClusterConfig _clusterConfig; 13 | protected DhafInternalConfig _dhafInternalConfig; 14 | private readonly string _extensionSign; 15 | 16 | protected string _etcdRootPath 17 | { 18 | get => $"/{_clusterConfig.Dhaf.ClusterName}/{_dhafInternalConfig.Etcd.ExtensionStoragePath}/{_extensionSign}/"; 19 | } 20 | 21 | public ExtensionStorageProvider(EtcdClient etcdClient, Grpc.Core.Metadata etcdHeaders, 22 | ClusterConfig clusterConfig, DhafInternalConfig dhafInternalConfig, 23 | string extensionPrefix) 24 | { 25 | _etcdClient = etcdClient; 26 | _etcdHeaders = etcdHeaders; 27 | _clusterConfig = clusterConfig; 28 | _dhafInternalConfig = dhafInternalConfig; 29 | _extensionSign = extensionPrefix; 30 | } 31 | 32 | public async Task DeleteAsync(string key) 33 | { 34 | var realKey = _etcdRootPath + key; 35 | await _etcdClient.DeleteAsync(realKey, _etcdHeaders); 36 | } 37 | 38 | public async Task DeleteRangeAsync(string keyPrefix) 39 | { 40 | var realKeyPrefix = _etcdRootPath + keyPrefix; 41 | await _etcdClient.DeleteRangeAsync(realKeyPrefix, _etcdHeaders); 42 | } 43 | 44 | public async Task GetAsyncOfDefault(string key) 45 | { 46 | var realKey = _etcdRootPath + key; 47 | var value = await _etcdClient.GetValAsync(realKey, _etcdHeaders); 48 | 49 | if (string.IsNullOrEmpty(value)) 50 | { 51 | return null; 52 | } 53 | 54 | return value; 55 | } 56 | 57 | public async Task> GetRangeAsync(string keyPrefix) 58 | { 59 | var realKeyPrefix = _etcdRootPath + keyPrefix; 60 | 61 | var values = await _etcdClient.GetRangeValAsync(realKeyPrefix, _etcdHeaders); 62 | return values; 63 | } 64 | 65 | public async Task PutAsync(string key, string value) 66 | { 67 | var realKey = _etcdRootPath + key; 68 | await _etcdClient.PutAsync(realKey, value, _etcdHeaders); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooSwitcher/FooSwitcher.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dhaf.Switchers.Foo 8 | { 9 | public class FooSwitcher : ISwitcher 10 | { 11 | private ILogger _logger; 12 | 13 | private Config _config; 14 | private InternalConfig _internalConfig; 15 | private ClusterServiceConfig _serviceConfig; 16 | 17 | protected string _currentEntryPointId = string.Empty; 18 | 19 | public string ExtensionName => "foo"; 20 | public string Sign => $"[{_serviceConfig.Name}/{ExtensionName} sw]"; 21 | 22 | public Type ConfigType => typeof(Config); 23 | public Type InternalConfigType => typeof(InternalConfig); 24 | 25 | public async Task Init(SwitcherInitOptions options) 26 | { 27 | _serviceConfig = options.ClusterServiceConfig; 28 | _logger = options.Logger; // The dhaf extension system provides logging out of the box. 29 | 30 | _logger.LogTrace($"{Sign} Init process..."); 31 | 32 | // The dhaf extension system will automatically populate instances of the configuration classes. 33 | _config = (Config)options.Config; 34 | _internalConfig = (InternalConfig)options.InternalConfig; 35 | 36 | // <- Other code to initialize your extension HERE. 37 | // <- Can be empty if you have nothing else to initialize. 38 | 39 | _logger.LogInformation($"{Sign} Init OK."); 40 | } 41 | 42 | public async Task Switch(SwitcherSwitchOptions options) 43 | { 44 | var entryPoint = _serviceConfig.EntryPoints.FirstOrDefault(x => x.Id == options.EntryPointId); 45 | _logger.LogInformation($"{Sign} Switch to entry point <{entryPoint.Id}> requested..."); 46 | 47 | // <- Your code for the switch is HERE. 48 | 49 | _currentEntryPointId = entryPoint.Id; 50 | _logger.LogInformation($"{Sign} Successfully switched to entry point <{entryPoint.Id}>."); 51 | } 52 | 53 | public async Task DhafNodeRoleChangedEventHandler(DhafNodeRole role) 54 | { 55 | // <- Your code to react to the event of the current 56 | // <- dhaf cluster node role change (can be empty in most cases). 57 | } 58 | 59 | public async Task GetCurrentEntryPointId() 60 | { 61 | // In some cases can be replaced by more complex logic. 62 | return _currentEntryPointId; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Dhaf.Node/Program.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using Dhaf.Core; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using NLog; 8 | using NLog.Extensions.Logging; 9 | using System; 10 | using System.Threading.Tasks; 11 | 12 | namespace Dhaf.Node 13 | { 14 | class Program 15 | { 16 | static async Task Main(string[] args) 17 | { 18 | var logger = LogManager.GetCurrentClassLogger(); 19 | 20 | try 21 | { 22 | await CreateHostBuilder(args).Build().RunAsync(); 23 | } 24 | catch (ConfigParsingException ex) 25 | { 26 | logger.Fatal($"Config parsing error {ex.Code}: {ex.Message}"); 27 | } 28 | catch (YamlDotNet.Core.YamlException ex) 29 | { 30 | logger.Fatal($"Config YAML deserialize error:\n{ex.Message}"); 31 | } 32 | /*catch (Exception ex) 33 | { 34 | logger.Fatal($"Further work of the node is impossible because of a fatal error:\n{ex.Message}"); 35 | }*/ 36 | finally 37 | { 38 | logger.Info("* Dhaf node exit..."); 39 | LogManager.Shutdown(); 40 | } 41 | } 42 | 43 | public static IHostBuilder CreateHostBuilder(string[] args) => 44 | Host.CreateDefaultBuilder(args) 45 | .ConfigureAppConfiguration((hostingContext, config) => 46 | { 47 | config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); 48 | config.AddEnvironmentVariables(); 49 | }) 50 | .ConfigureServices(services => 51 | { 52 | 53 | services.AddSingleton(servicesProvider => 54 | { 55 | ArgsOptions argsOptions = null; 56 | Parser.Default.ParseArguments(args) 57 | .WithParsed(p => argsOptions = p); 58 | 59 | return argsOptions; 60 | }) 61 | .AddHostedService() 62 | .AddLogging(loggingBuilder => 63 | { 64 | loggingBuilder.ClearProviders(); 65 | loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); 66 | loggingBuilder.AddNLog(); 67 | }); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /templates/agents/google-cloud/README.md: -------------------------------------------------------------------------------- 1 | # Google Cloud Agents 2 | ⚠️ Before reading the current document, read the main [README.md](../../README.md#with-google-cloud-switcher-provider) (in the "Quick Start/With Google Cloud switcher provider" section). 3 | 4 | So, here are the agents that allow you to make a Google Cloud VM instance a reverse proxy that proxies to the server specified in the GC project metadata. 5 | In general, you can implement this task in any way you want. However, we offer ready-made templates below. 6 | 7 | ## Nginx agent 8 | Look at the nginx [subdirectory](nginx). Two files can be found there: 9 | 1. `nginx_template.conf` - a basic configuration for nginx that makes it a reverse proxy to the server specified in the static variable `$dhaf_ip`; 10 | 2. `dhaf_serv1_ip` - the file that contains the definition of the variable `$dhaf_ip`. It will be statically included to the main configuration file above. 11 | 12 | Then look at the `gc-nginx-agent.py` file in the current directory. 13 | It is a simple python (v3) script that monitors changes in Google Cloud project metadata, and, 14 | if necessary, passes those changes to nginx (and also reloads its configuration). In other words, it fills the `dhaf_serv1_ip` file described above (and similar). 15 | This does not require you to prepare any credentials, because inside the VM instance, project metadata is available by default 16 | Aslo, it supports working for multiple services in dhaf at once. 17 | 18 | In the directory with the script you must place the file `services.yaml`, inside which is a collection where each item contains: 19 | 1. Service name. Does not have a strong importance, implemented simply for usability; 20 | 2. Goolge Cloud Metadata key. The same key must be specified in the dhaf service configuration; 21 | 3. The path to the nginx subconfig (similar to `dhaf_serv1_ip` described above) which will contain the current ip address variable (`$dhaf_ip`) definition for proxying. 22 | 23 | For example: 24 | ```yaml 25 | - name: serv1 26 | gcmd_key: dhaf_serv1_ip 27 | nginx_subconfig_path: /etc/nginx/dhaf/dhaf_serv1_ip 28 | ``` 29 | 30 | That's all. Now you can make this script run with the OS, for example using `systemd`. You can find a ready-made template [here](systemd). 31 | 32 | Oh yes, if you want to use the autoscaling feature of the [managed instance group](https://cloud.google.com/compute/docs/instance-groups#managed_instance_groups) (it's generally not necessary, 33 | you can leave a static number of instances and disallow automatic changes), 34 | don't forget to describe in the Google Cloud instance template the [startup-script](https://cloud.google.com/compute/docs/instance-templates/deterministic-instance-templates) 35 | for installation of nginx and the corresponding agent (and so on). You can also use a container (e.g. a [docker](https://www.docker.com/)) for this. Good luck ;) 36 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Exec/ExecHealthChecker.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dhaf.HealthCheckers.Exec 8 | { 9 | /// 10 | /// The exec health checker can run any command, and expects an zero-value exit code on success. 11 | /// Non-zero exit codes are considered errors. 12 | /// 13 | public class ExecHealthChecker : IHealthChecker 14 | { 15 | private ILogger _logger; 16 | 17 | private Config _config; 18 | private InternalConfig _internalConfig; 19 | private ClusterServiceConfig _serviceConfig; 20 | 21 | public string ExtensionName => "exec"; 22 | 23 | public Type ConfigType => typeof(Config); 24 | public Type InternalConfigType => typeof(InternalConfig); 25 | 26 | public string Sign => $"[{_serviceConfig.Name}/{ExtensionName} hc]"; 27 | 28 | public async Task Init(HealthCheckerInitOptions options) 29 | { 30 | _serviceConfig = options.ClusterServiceConfig; 31 | _logger = options.Logger; 32 | 33 | _logger.LogTrace($"{Sign} Init process..."); 34 | 35 | _config = (Config)options.Config; 36 | _internalConfig = (InternalConfig)options.InternalConfig; 37 | 38 | var execResults = Shell.Exec(_config.Init); 39 | 40 | if (!execResults.Success || execResults.ExitCode != 0) 41 | { 42 | _logger.LogCritical($"{Sign} The executable initialization file returned a non-zero return code."); 43 | throw new ExtensionInitFailedException(Sign); 44 | } 45 | 46 | _logger.LogTrace($"{Sign} Init output: <{execResults.Output}>"); 47 | _logger.LogTrace($"{Sign} Init total exec time: {execResults.TotalExecuteTime} ms."); 48 | _logger.LogInformation($"{Sign} Init OK."); 49 | } 50 | 51 | public async Task Check(HealthCheckerCheckOptions options) 52 | { 53 | var entryPoint = _serviceConfig.EntryPoints.FirstOrDefault(x => x.Id == options.EntryPointId); 54 | 55 | var args = $"{entryPoint.Id} {entryPoint.IP}"; 56 | var execResults = Shell.Exec(_config.Check); 57 | 58 | if (execResults.Success && execResults.ExitCode == 0) 59 | { 60 | return new HealthStatus { Healthy = true }; 61 | } 62 | 63 | return new HealthStatus { Healthy = false }; 64 | } 65 | 66 | public async Task DhafNodeRoleChangedEventHandler(DhafNodeRole role) { } 67 | 68 | public async Task ResolveUnhealthinessReasonCode(int code) 69 | { 70 | return "The return code was different from <0>."; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Actions/StatusService.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Node; 2 | using RestSharp; 3 | using Spectre.Console; 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Dhaf.CLI 9 | { 10 | public static partial class Actions 11 | { 12 | public static async Task ExecuteStatusServiceAndReturnExitCode(StatusServiceOptions opt) 13 | { 14 | const int TABLE_WIDTH = 80; 15 | 16 | await PrepareRestClient(opt); 17 | var request = new RestRequest($"service/status?serviceName={opt.ServiceName}"); 18 | 19 | var response = await _restClient.GetAsync>(request); 20 | if (!response.Success) 21 | { 22 | PrintErrors(response.Errors); 23 | return -1; 24 | } 25 | 26 | var serviceStatus = response.Data; 27 | var isUp = serviceStatus.EntryPoints 28 | .Any(x => x.Healthy) ? "[green]UP[/]" : "[red]DOWN[/]"; 29 | 30 | var sw = serviceStatus.SwitchoverRequirement is null ? "NO" : $"YES (to <{serviceStatus.SwitchoverRequirement}>)"; 31 | 32 | var summaryTable = new Table 33 | { 34 | Width = TABLE_WIDTH 35 | }; 36 | 37 | summaryTable.Border(TableBorder.Ascii2); 38 | summaryTable.Title = new TableTitle($"Service \"{serviceStatus.Name}\" -> Summary"); 39 | summaryTable.AddColumns("Key", "Value"); 40 | summaryTable.AddRow("Service status", isUp); 41 | summaryTable.AddRow("Domain", serviceStatus.Domain); 42 | summaryTable.AddRow("Switchover is required", sw); 43 | AnsiConsole.Write(summaryTable); 44 | 45 | Console.WriteLine(); 46 | var epTable = new Table 47 | { 48 | Width = TABLE_WIDTH 49 | }; 50 | 51 | epTable.Border(TableBorder.Ascii2); 52 | epTable.Title = new TableTitle($"Service \"{serviceStatus.Name}\" -> Entry points"); 53 | epTable.AddColumns("Priority", "Name", "Healthy", "Status"); 54 | epTable.Columns[0].Centered(); 55 | epTable.Columns[1].Centered(); 56 | epTable.Columns[2].Centered(); 57 | epTable.Columns[3].Centered(); 58 | 59 | foreach (var ep in serviceStatus.EntryPoints) 60 | { 61 | var status = ep.Healthy ? "READY" : "DISABLED"; 62 | if (ep.Name == serviceStatus.CurrentEntryPointName) 63 | { 64 | status = "[white]CURRENT[/]"; 65 | } 66 | 67 | epTable.AddRow(ep.Priority.ToString(), ep.Name, 68 | ep.Healthy ? "[green]Yes[/]" : "[red]No[/]", status); 69 | } 70 | 71 | AnsiConsole.Write(epTable); 72 | return 0; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /templates/dhaf_extensions/Dhaf.Templates.Extensions.FooHealthChecker/FooHealthChecker.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dhaf.HealthCheckers.Foo 8 | { 9 | public class FooHealthChecker : IHealthChecker 10 | { 11 | private ILogger _logger; 12 | 13 | private Config _config; 14 | private InternalConfig _internalConfig; 15 | private ClusterServiceConfig _serviceConfig; 16 | 17 | public string ExtensionName => "foo"; 18 | public string Sign => $"[{_serviceConfig.Name}/{ExtensionName} hc]"; 19 | 20 | public Type ConfigType => typeof(Config); 21 | public Type InternalConfigType => typeof(InternalConfig); 22 | 23 | public async Task Init(HealthCheckerInitOptions options) 24 | { 25 | _serviceConfig = options.ClusterServiceConfig; 26 | _logger = options.Logger; // The dhaf extension system provides logging out of the box. 27 | 28 | _logger.LogTrace($"{Sign} Init process..."); 29 | 30 | // The dhaf extension system will automatically populate instances of the configuration classes. 31 | _config = (Config)options.Config; 32 | _internalConfig = (InternalConfig)options.InternalConfig; 33 | 34 | // <- Other code to initialize your extension HERE. 35 | // <- Can be empty if you have nothing else to initialize. 36 | 37 | _logger.LogInformation($"{Sign} Init OK."); 38 | } 39 | 40 | public async Task Check(HealthCheckerCheckOptions options) 41 | { 42 | var entryPoint = _serviceConfig.EntryPoints.FirstOrDefault(x => x.Id == options.EntryPointId); 43 | 44 | // <- Your health check code is HERE. 45 | 46 | // This is the test data for the return. It must be replaced with real data. 47 | var reasonCode = 1; // The code for the cause of unhealthiness (for illustration purposes only). 48 | return new HealthStatus { Healthy = false, ReasonCode = reasonCode }; 49 | } 50 | 51 | public async Task DhafNodeRoleChangedEventHandler(DhafNodeRole role) 52 | { 53 | // <- Your code to react to the event of the current 54 | // <- dhaf cluster node role change (can be empty in most cases). 55 | } 56 | 57 | 58 | public async Task ResolveUnhealthinessReasonCode(int code) 59 | { 60 | // The dhaf cluster state store stores the reason of unhealthiness as an integer code. 61 | // This method serves to resolve such codes into human-readable text. 62 | // How to do it is up to you. 63 | // For example, you can store a Dictionary that resolves codes into text. 64 | // And you can also use enums. 65 | 66 | return "Test reason"; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Dhaf.Node/RestApi/RestApiFactory.cs: -------------------------------------------------------------------------------- 1 | using EmbedIO; 2 | using EmbedIO.Utilities; 3 | using EmbedIO.WebApi; 4 | using Microsoft.Extensions.Logging; 5 | using Swan.Logging; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Text.Json; 9 | using System.Threading.Tasks; 10 | 11 | namespace Dhaf.Node 12 | 13 | { 14 | public class RestApiFactory 15 | { 16 | protected static async Task SerializationCallback(IHttpContext context, object data) 17 | { 18 | Validate.NotNull(nameof(context), context).Response.ContentType = MimeType.Json; 19 | using var text = context.OpenResponseText(new UTF8Encoding(false)); 20 | await text.WriteAsync(JsonSerializer.Serialize(data, new JsonSerializerOptions() 21 | { 22 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 23 | })).ConfigureAwait(false); 24 | } 25 | 26 | public WebServer CreateWebServer(string url, IDhafNode dhafNode, ILogger logger) 27 | { 28 | Logger.NoLogging(); 29 | 30 | var server = new WebServer(o => o 31 | .WithUrlPrefix(url) 32 | .WithMode(HttpListenerMode.EmbedIO)) 33 | .WithLocalSessionManager() 34 | .WithWebApi("/", SerializationCallback, 35 | m => m.WithController(() => new RestApiController(dhafNode, logger)) 36 | ); 37 | 38 | server.HandleUnhandledException(async (context, exception) => 39 | { 40 | context.Response.StatusCode = 500; 41 | var errors = new List() 42 | { 43 | new RestApiError { Code = -1, Message = "Internal error." } 44 | }; 45 | 46 | if (exception is RestApiException restApiExp) 47 | { 48 | context.Response.StatusCode = 400; 49 | errors = new List() 50 | { 51 | new RestApiError { Code = restApiExp.Code, Message = restApiExp.Message } 52 | }; 53 | } 54 | 55 | await context.SendDataAsync(SerializationCallback, new RestApiResponse 56 | { 57 | Success = false, 58 | Errors = errors 59 | }); 60 | }); 61 | 62 | server.HandleHttpException(async (context, exception) => 63 | { 64 | context.Response.StatusCode = 400; 65 | 66 | var msg = exception.StatusCode == 404 ? "Endpoint not found." : exception.Message; 67 | var errors = new List() 68 | { 69 | new RestApiError { Code = exception.StatusCode, Message = msg } 70 | }; 71 | 72 | await context.SendDataAsync(SerializationCallback, new RestApiResponse 73 | { 74 | Success = false, 75 | Errors = errors 76 | }); 77 | }); 78 | 79 | return server; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Dhaf.CLI/Actions/StatusServices.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Node; 2 | using RestSharp; 3 | using Spectre.Console; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Dhaf.CLI 10 | { 11 | public static partial class Actions 12 | { 13 | public static async Task ExecuteStatusServicesAndReturnExitCode(StatusServicesOptions opt) 14 | { 15 | const int TABLE_WIDTH = 80; 16 | 17 | await PrepareRestClient(opt); 18 | var request = new RestRequest($"services/status"); 19 | 20 | var response = await _restClient.GetAsync>>(request); 21 | if (!response.Success) 22 | { 23 | PrintErrors(response.Errors); 24 | return -1; 25 | } 26 | 27 | foreach (var serviceStatus in response.Data) 28 | { 29 | var isUp = serviceStatus.EntryPoints 30 | .Any(x => x.Healthy) ? "[green]UP[/]" : "[red]DOWN[/]"; 31 | 32 | var sw = serviceStatus.SwitchoverRequirement is null ? "NO" : $"YES (to <{serviceStatus.SwitchoverRequirement}>)"; 33 | 34 | var summaryTable = new Table 35 | { 36 | Width = TABLE_WIDTH 37 | }; 38 | 39 | summaryTable.Border(TableBorder.Ascii2); 40 | summaryTable.Title = new TableTitle($"Service \"{serviceStatus.Name}\" -> Summary"); 41 | summaryTable.AddColumns("Key", "Value"); 42 | summaryTable.AddRow("Service status", isUp); 43 | summaryTable.AddRow("Domain", serviceStatus.Domain); 44 | summaryTable.AddRow("Switchover is required", sw); 45 | AnsiConsole.Write(summaryTable); 46 | 47 | Console.WriteLine(); 48 | var epTable = new Table 49 | { 50 | Width = TABLE_WIDTH 51 | }; 52 | 53 | epTable.Border(TableBorder.Ascii2); 54 | epTable.Title = new TableTitle($"Service \"{serviceStatus.Name}\" -> Entry points"); 55 | epTable.AddColumns("Priority", "Name", "Healthy", "Status"); 56 | epTable.Columns[0].Centered(); 57 | epTable.Columns[1].Centered(); 58 | epTable.Columns[2].Centered(); 59 | epTable.Columns[3].Centered(); 60 | 61 | foreach (var nc in serviceStatus.EntryPoints) 62 | { 63 | var status = nc.Healthy ? "READY" : "DISABLED"; 64 | if (nc.Name == serviceStatus.CurrentEntryPointName) 65 | { 66 | status = "[white]CURRENT[/]"; 67 | } 68 | 69 | epTable.AddRow(nc.Priority.ToString(), nc.Name, 70 | nc.Healthy ? "[green]Yes[/]" : "[red]No[/]", status); 71 | } 72 | 73 | AnsiConsole.Write(epTable); 74 | 75 | const char SEP_CHAR = '='; 76 | Console.WriteLine(new string(SEP_CHAR, TABLE_WIDTH) + "\n"); 77 | } 78 | 79 | return 0; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Dhaf.Node/RestApi/RestApiController.cs: -------------------------------------------------------------------------------- 1 | using EmbedIO; 2 | using EmbedIO.Routing; 3 | using EmbedIO.WebApi; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | namespace Dhaf.Node 10 | { 11 | public class RestApiController : WebApiController 12 | { 13 | protected readonly IDhafNode _dhafNode; 14 | protected readonly ILogger _logger; 15 | 16 | public RestApiController() 17 | { 18 | throw new Exception("The default constructor is not possible."); 19 | } 20 | 21 | public RestApiController(IDhafNode dhafNode, ILogger logger) 22 | { 23 | _dhafNode = dhafNode; 24 | _logger = logger; 25 | } 26 | 27 | 28 | [Route(HttpVerbs.Get, "/ping")] 29 | public async Task Ping() 30 | { 31 | return "pong"; 32 | } 33 | 34 | [Route(HttpVerbs.Get, "/switchover")] 35 | public async Task Switchover([QueryField(true)] string serviceName, [QueryField(true)] string entryPointId) 36 | { 37 | await _dhafNode.Switchover(serviceName, entryPointId); 38 | return new RestApiResponse { Success = true }; 39 | } 40 | 41 | [Route(HttpVerbs.Get, "/switchover/purge")] 42 | public async Task SwitchoverPurge([QueryField(true)] string serviceName) 43 | { 44 | await _dhafNode.PurgeSwitchover(serviceName); 45 | return new RestApiResponse { Success = true }; 46 | } 47 | 48 | [Route(HttpVerbs.Get, "/switchover/candidates")] 49 | public async Task SwitchoverСandidates([QueryField(true)] string serviceName) 50 | { 51 | var candidates = await _dhafNode.GetSwitchoverCandidates(serviceName); 52 | return new RestApiResponse> { Success = true, Data = candidates }; 53 | } 54 | 55 | [Route(HttpVerbs.Get, "/service/status")] 56 | public async Task ServiceStatus([QueryField(true)] string serviceName) 57 | { 58 | var status = await _dhafNode.GetServiceStatus(serviceName); 59 | return new RestApiResponse { Success = true, Data = status }; 60 | } 61 | 62 | [Route(HttpVerbs.Get, "/services/status")] 63 | public async Task ServicesStatus() 64 | { 65 | var statuses = await _dhafNode.GetServicesStatus(); 66 | return new RestApiResponse> { Success = true, Data = statuses }; 67 | } 68 | 69 | [Route(HttpVerbs.Get, "/dhaf/status")] 70 | public async Task DhafStatus() 71 | { 72 | var status = await _dhafNode.GetDhafClusterStatus(); 73 | return new RestApiResponse { Success = true, Data = status }; 74 | } 75 | 76 | [Route(HttpVerbs.Get, "/dhaf/node/decommission")] 77 | public async Task DhafNodeDecommission([QueryField(true)] string name) 78 | { 79 | await _dhafNode.DecommissionDhafNode(name); 80 | return new RestApiResponse { Success = true }; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.Exec/ExecSwitcher.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dhaf.Switchers.Exec 8 | { 9 | public class ExecSwitcher : ISwitcher 10 | { 11 | private ILogger _logger; 12 | 13 | private Config _config; 14 | private InternalConfig _internalConfig; 15 | private ClusterServiceConfig _serviceConfig; 16 | 17 | protected string _currentEntryPointId = string.Empty; 18 | 19 | public string ExtensionName => "exec"; 20 | 21 | public Type ConfigType => typeof(Config); 22 | public Type InternalConfigType => typeof(InternalConfig); 23 | 24 | public string Sign => $"[{_serviceConfig.Name}/{ExtensionName} sw]"; 25 | 26 | public async Task GetCurrentEntryPointId() 27 | { 28 | return _currentEntryPointId; 29 | } 30 | 31 | public async Task Init(SwitcherInitOptions options) 32 | { 33 | _serviceConfig = options.ClusterServiceConfig; 34 | _logger = options.Logger; 35 | 36 | _logger.LogTrace($"{Sign} Init process..."); 37 | 38 | _config = (Config)options.Config; 39 | _internalConfig = (InternalConfig)options.InternalConfig; 40 | 41 | var execResults = Shell.Exec(_config.Init); 42 | 43 | if (!execResults.Success || execResults.ExitCode != 0) 44 | { 45 | _logger.LogCritical($"{Sign} The executable initialization file returned a non-zero return code."); 46 | throw new ExtensionInitFailedException(Sign); 47 | } 48 | 49 | _logger.LogTrace($"{Sign} Init output: <{execResults.Output}>"); 50 | _logger.LogTrace($"{Sign} Init total exec time: {execResults.TotalExecuteTime} ms."); 51 | 52 | // The exec switcher MUST return the ID of the current entry point 53 | // if it initializes successfully. 54 | var currentEpId = execResults.Output.Trim(); 55 | _currentEntryPointId = currentEpId; 56 | 57 | _logger.LogInformation($"{Sign} Init OK."); 58 | } 59 | 60 | public async Task Switch(SwitcherSwitchOptions options) 61 | { 62 | var entryPoint = _serviceConfig.EntryPoints.FirstOrDefault(x => x.Id == options.EntryPointId); 63 | _logger.LogInformation($"{Sign} Switch to entry point <{entryPoint.Id}> requested..."); 64 | 65 | var args = $"{entryPoint.Id} {entryPoint.IP}"; 66 | var execResults = Shell.Exec(_config.Switch, args); 67 | 68 | if (!execResults.Success || execResults.ExitCode != 0) 69 | { 70 | _logger.LogCritical($"{Sign} The executable for the switch returned a non-zero return code."); 71 | throw new SwitchFailedException(Sign); 72 | } 73 | 74 | _logger.LogTrace($"{Sign} Switch output: <{execResults.Output}>"); 75 | _logger.LogTrace($"{Sign} Switch total exec time: {execResults.TotalExecuteTime} ms."); 76 | 77 | _currentEntryPointId = entryPoint.Id; 78 | _logger.LogInformation($"{Sign} Successfully switched to entry point <{entryPoint.Id}>."); 79 | } 80 | 81 | public async Task DhafNodeRoleChangedEventHandler(DhafNodeRole role) { } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Dhaf.HealthCheckers.Tcp/TcpHealthChecker.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Linq; 5 | using System.Net.Sockets; 6 | using System.Threading.Tasks; 7 | 8 | namespace Dhaf.HealthCheckers.Tcp 9 | { 10 | public class TcpHealthChecker : IHealthChecker 11 | { 12 | private ILogger _logger; 13 | 14 | private Config _config; 15 | private InternalConfig _internalConfig; 16 | private ClusterServiceConfig _serviceConfig; 17 | 18 | public string ExtensionName => "tcp"; 19 | 20 | public Type ConfigType => typeof(Config); 21 | public Type InternalConfigType => typeof(InternalConfig); 22 | 23 | public string Sign => $"[{_serviceConfig.Name}/{ExtensionName} hc]"; 24 | 25 | public async Task Init(HealthCheckerInitOptions options) 26 | { 27 | _serviceConfig = options.ClusterServiceConfig; 28 | _logger = options.Logger; 29 | 30 | _logger.LogTrace($"{Sign} Init process..."); 31 | 32 | _config = (Config)options.Config; 33 | _internalConfig = (InternalConfig)options.InternalConfig; 34 | 35 | await ConfigCheck(); 36 | _logger.LogInformation($"{Sign} Init OK."); 37 | } 38 | 39 | public async Task Check(HealthCheckerCheckOptions options) 40 | { 41 | var entryPoint = _serviceConfig.EntryPoints.FirstOrDefault(x => x.Id == options.EntryPointId); 42 | 43 | try 44 | { 45 | const int MS_IN_SEC = 1000; 46 | 47 | using var tcpClient = new TcpClient(); 48 | tcpClient.ReceiveTimeout = (_config.ReceiveTimeout ?? _internalConfig.DefReceiveTimeout) * MS_IN_SEC; 49 | 50 | await tcpClient.ConnectAsync(entryPoint.IP, _config.Port.Value); 51 | 52 | return new HealthStatus { Healthy = true }; 53 | } 54 | catch (SocketException ex) 55 | { 56 | return new HealthStatus 57 | { 58 | Healthy = false, 59 | ReasonCode = (int)ex.SocketErrorCode 60 | }; 61 | } 62 | } 63 | 64 | public async Task DhafNodeRoleChangedEventHandler(DhafNodeRole role) { } 65 | 66 | public async Task ResolveUnhealthinessReasonCode(int code) 67 | { 68 | if (Enum.IsDefined(typeof(SocketError), code)) 69 | { 70 | var socketError = (SocketError)code; 71 | return socketError.ToString(); 72 | } 73 | 74 | return "Unexpected reason"; 75 | } 76 | 77 | protected async Task ConfigCheck() 78 | { 79 | if (_config.Port is null) 80 | { 81 | throw new ConfigParsingException(1901, $"{Sign} The port is not specified."); 82 | } 83 | 84 | if (_config.ReceiveTimeout is not null 85 | && (_config.ReceiveTimeout < _internalConfig.MinReceiveTimeout 86 | || _config.ReceiveTimeout > _internalConfig.MaxReceiveTimeout)) 87 | { 88 | throw new ConfigParsingException(1902, $"{Sign} Receive timeout must be in the " + 89 | $"range {_internalConfig.MinReceiveTimeout}-{_internalConfig.MaxReceiveTimeout} seconds."); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /templates/build_example.sh: -------------------------------------------------------------------------------- 1 | # runtime identifier: win-x64, linux-x64, osx-x64 (see https://docs.microsoft.com/en-us/dotnet/core/rid-catalog for details) 2 | RID= 3 | DOTNET_CLI_TELEMETRY_OPTOUT=1 4 | 5 | # clone .git repo 6 | git clone https://github.com/hyperion-cs/dhaf.git 7 | cd dhaf 8 | 9 | # restore dependencies 10 | dotnet restore src/Dhaf.sln 11 | 12 | # build dhaf core, node, cli 13 | dotnet publish src/Dhaf.Core/Dhaf.Core.csproj --configuration Release --no-restore -nowarn:CS1998 -r "$RID" -o "bin/$RID/core" /p:DebugType=None /p:DebugSymbols=false 14 | dotnet publish src/Dhaf.Node/Dhaf.Node.csproj --configuration Release --no-dependencies --no-restore -nowarn:CS1998 -r "$RID" -o "bin/$RID" -p:PublishSingleFile=true --self-contained false /p:DebugType=None /p:DebugSymbols=false 15 | dotnet publish src/Dhaf.CLI/Dhaf.CLI.csproj --configuration Release --no-dependencies --no-restore -nowarn:CS1998 -r "$RID" -o "bin/$RID" -p:PublishSingleFile=true --self-contained false /p:DebugType=None /p:DebugSymbols=false 16 | 17 | # build core extensions 18 | 19 | dotnet publish src/Dhaf.HealthCheckers.Web/Dhaf.HealthCheckers.Web.csproj --configuration Release --no-restore -nowarn:CS1998 -r "$RID" -o "bin/$RID/ext/hc/web" /p:DebugType=None /p:DebugSymbols=false 20 | dotnet publish src/Dhaf.HealthCheckers.Exec/Dhaf.HealthCheckers.Exec.csproj --configuration Release --no-restore -nowarn:CS1998 -r $RID -o "bin/$RID/ext/hc/exec" /p:DebugType=None /p:DebugSymbols=false 21 | 22 | dotnet publish src/Dhaf.Switchers.Cloudflare/Dhaf.Switchers.Cloudflare.csproj --configuration Release --no-restore -nowarn:CS1998 -r "$RID" -o "bin/$RID/ext/sw/cloudflare" /p:DebugType=None /p:DebugSymbols=false 23 | dotnet publish src/Dhaf.Switchers.GoogleCloud/Dhaf.Switchers.GoogleCloud.csproj --configuration Release --no-restore -nowarn:CS1998 -r "$RID" -o "bin/$RID/ext/sw/google-cloud" /p:DebugType=None /p:DebugSymbols=false 24 | dotnet publish src/Dhaf.Switchers.Exec/Dhaf.Switchers.Exec.csproj --configuration Release --no-restore -nowarn:CS1998 -r "$RID" -o "bin/$RID/ext/sw/exec" /p:DebugType=None /p:DebugSymbols=false 25 | 26 | dotnet publish src/Dhaf.Notifiers.Email/Dhaf.Notifiers.Email.csproj --configuration Release --no-restore -nowarn:CS1998 -r "$RID" -o "bin/$RID/ext/ntf/email" /p:DebugType=None /p:DebugSymbols=false 27 | dotnet publish src/Dhaf.Notifiers.Telegram/Dhaf.Notifiers.Telegram.csproj --configuration Release --no-restore -nowarn:CS1998 -r "$RID" -o "bin/$RID/ext/ntf/tg" /p:DebugType=None /p:DebugSymbols=false 28 | 29 | # "$RID-artifacts" — the build destination folder. 30 | 31 | mkdir -p $RID-artifacts 32 | mkdir -p $RID-artifacts/libs 33 | mkdir -p $RID-artifacts/extensions/health-checkers/web 34 | mkdir -p $RID-artifacts/extensions/health-checkers/exec 35 | mkdir -p $RID-artifacts/extensions/switchers/cloudflare 36 | mkdir -p $RID-artifacts/extensions/switchers/google-cloud 37 | mkdir -p $RID-artifacts/extensions/switchers/exec 38 | mkdir -p $RID-artifacts/extensions/notifiers/email 39 | mkdir -p $RID-artifacts/extensions/notifiers/tg 40 | 41 | mv ./bin/$RID/Dhaf.Node $RID-artifacts/dhaf.node 42 | mv ./bin/$RID/Dhaf.CLI $RID-artifacts/dhaf.cli 43 | mv ./bin/$RID/appsettings.json ./bin/$RID/nlog.config $RID-artifacts 44 | mv ./bin/$RID/core/* $RID-artifacts/libs 45 | mv ./bin/$RID/ext/hc/web/* $RID-artifacts/extensions/health-checkers/web 46 | mv ./bin/$RID/ext/hc/exec/* $RID-artifacts/extensions/health-checkers/exec 47 | mv ./bin/$RID/ext/sw/cloudflare/* $RID-artifacts/extensions/switchers/cloudflare 48 | mv ./bin/$RID/ext/sw/google-cloud/* $RID-artifacts/extensions/switchers/google-cloud 49 | mv ./bin/$RID/ext/sw/exec/* $RID-artifacts/extensions/switchers/exec 50 | mv ./bin/$RID/ext/ntf/email/* $RID-artifacts/extensions/notifiers/email 51 | mv ./bin/$RID/ext/ntf/tg/* $RID-artifacts/extensions/notifiers/tg 52 | -------------------------------------------------------------------------------- /src/Dhaf.Switchers.GoogleCloud/GoogleCloudSwitcher.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using Google.Apis.Auth.OAuth2; 3 | using Google.Apis.Compute.v1; 4 | using Google.Apis.Services; 5 | using Microsoft.Extensions.Logging; 6 | using System; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace Dhaf.Switchers.GoogleCloud 11 | { 12 | public class GoogleCloudSwitcher : ISwitcher 13 | { 14 | private ILogger _logger; 15 | 16 | private Config _config; 17 | private InternalConfig _internalConfig; 18 | private ClusterServiceConfig _serviceConfig; 19 | 20 | protected string _currentEntryPointId = string.Empty; 21 | 22 | protected ComputeService _gcComputeService; 23 | protected ProjectsResource _gcProjectsResource; 24 | 25 | public string ExtensionName => "google-cloud"; 26 | 27 | public Type ConfigType => typeof(Config); 28 | public Type InternalConfigType => typeof(InternalConfig); 29 | 30 | public string Sign => $"[{_serviceConfig.Name}/{ExtensionName} sw]"; 31 | 32 | public async Task Init(SwitcherInitOptions options) 33 | { 34 | _serviceConfig = options.ClusterServiceConfig; 35 | _logger = options.Logger; 36 | 37 | _logger.LogTrace($"{Sign} Init process..."); 38 | 39 | _config = (Config)options.Config; 40 | _internalConfig = (InternalConfig)options.InternalConfig; 41 | 42 | var credential = GoogleCredential.FromFile(_config.CredentialsPath); 43 | _gcComputeService = new ComputeService(new BaseClientService.Initializer 44 | { 45 | HttpClientInitializer = credential 46 | }); 47 | 48 | _gcProjectsResource = new ProjectsResource(_gcComputeService); 49 | 50 | var gcProject = await _gcProjectsResource 51 | .Get(_config.Project) 52 | .ExecuteAsync(); 53 | 54 | var gcMetadataItem = gcProject.CommonInstanceMetadata.Items 55 | .FirstOrDefault(x => x.Key == _config.MetadataKey); 56 | 57 | if (gcMetadataItem is null) 58 | { 59 | _logger.LogCritical($"Project <{_config.Project}> does not have an initialization value in the metadata for key <{_config.MetadataKey}>."); 60 | throw new ExtensionInitFailedException(Sign); 61 | } 62 | 63 | var currentEntryPoint = _serviceConfig.EntryPoints.FirstOrDefault(x => x.IP == gcMetadataItem.Value); 64 | if (currentEntryPoint is null) 65 | { 66 | _logger.LogCritical($"In project <{_config.Project}>, the initialization value in the metadata for key <{_config.MetadataKey}> " + 67 | $"is incorrect because it does not match any of the entry points in the dhaf configuration." + 68 | $"On the other hand, it may be a mistake in the dhaf configuration."); 69 | 70 | throw new ExtensionInitFailedException(Sign); 71 | } 72 | 73 | _currentEntryPointId = currentEntryPoint.Id; 74 | _logger.LogInformation($"{Sign} Init OK."); 75 | } 76 | 77 | public async Task Switch(SwitcherSwitchOptions options) 78 | { 79 | var entryPoint = _serviceConfig.EntryPoints.FirstOrDefault(x => x.Id == options.EntryPointId); 80 | _logger.LogInformation($"{Sign} Switch to entry point <{entryPoint.Id}> requested..."); 81 | 82 | var gcProject = await _gcProjectsResource 83 | .Get(_config.Project) 84 | .ExecuteAsync(); 85 | 86 | var gcMetadata = gcProject.CommonInstanceMetadata; 87 | var currValue = gcMetadata.Items.FirstOrDefault(x => x.Key == _config.MetadataKey); 88 | 89 | if (currValue is null) 90 | { 91 | gcMetadata.Items.Add(new Google.Apis.Compute.v1.Data.Metadata.ItemsData 92 | { 93 | Key = _config.MetadataKey, 94 | Value = entryPoint.IP 95 | }); 96 | } 97 | else 98 | { 99 | currValue.Value = entryPoint.IP; 100 | } 101 | 102 | await _gcProjectsResource 103 | .SetCommonInstanceMetadata(gcMetadata, _config.Project) 104 | .ExecuteAsync(); 105 | 106 | _currentEntryPointId = entryPoint.Id; 107 | _logger.LogInformation($"{Sign} Successfully switched to entry point <{entryPoint.Id}>."); 108 | } 109 | 110 | public async Task GetCurrentEntryPointId() 111 | { 112 | return _currentEntryPointId; 113 | } 114 | 115 | public async Task DhafNodeRoleChangedEventHandler(DhafNodeRole role) { } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ClusterConfig/YamlResolving.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using YamlDotNet.Core; 5 | using YamlDotNet.Core.Events; 6 | using YamlDotNet.Serialization; 7 | using YamlDotNet.Serialization.NamingConventions; 8 | 9 | namespace Dhaf.Core 10 | { 11 | public class ExtensionConfigTypeMap 12 | { 13 | public string ExtensionName { get; set; } 14 | public Type ConfigTypeInterface { get; set; } 15 | public Type ImplType { get; set; } 16 | 17 | public ExtensionConfigTypeMap(Type configTypeInterface, Type implType) 18 | { 19 | ConfigTypeInterface = configTypeInterface; 20 | ImplType = implType; 21 | } 22 | 23 | public ExtensionConfigTypeMap(string extensionName, Type configTypeInterface, Type implType) 24 | { 25 | ExtensionName = extensionName; 26 | ConfigTypeInterface = configTypeInterface; 27 | ImplType = implType; 28 | } 29 | } 30 | 31 | public class ClusterConfigInterfacesResolver : INodeTypeResolver 32 | { 33 | protected Dictionary _map; 34 | 35 | public ClusterConfigInterfacesResolver(Dictionary map) 36 | { 37 | _map = map; 38 | } 39 | 40 | public bool Resolve(NodeEvent nodeEvent, ref Type type) 41 | { 42 | if (_map.TryGetValue(type, out var implementationType)) 43 | { 44 | type = implementationType; 45 | return true; 46 | } 47 | 48 | return false; 49 | } 50 | } 51 | 52 | public class ExtensionConfigYamlMark 53 | { 54 | public int AbsoluteOffset { get; set; } 55 | public Type InterfaceType { get; set; } 56 | public string ExtensionName { get; set; } 57 | } 58 | 59 | public enum EctcMode 60 | { 61 | CollectYamlMarks, ConvertWithMap 62 | } 63 | 64 | public class ExtensionConfigTypeConverter : IYamlTypeConverter 65 | { 66 | private readonly IDeserializer _deserializer; 67 | 68 | public List Map { get; set; } = new(); 69 | public EctcMode Mode { get; set; } = EctcMode.CollectYamlMarks; 70 | public List YamlMarks { get; set; } = new(); 71 | 72 | public List CatchTypes { get; set; } = new List() 73 | { 74 | typeof(SwitcherDefaultConfig), 75 | typeof(HealthCheckerDefaultConfig), 76 | typeof(NotifierDefaultConfig), 77 | }; 78 | 79 | public List AvailableInterfaces { get; set; } = new List() 80 | { 81 | typeof(ISwitcherConfig), 82 | typeof(IHealthCheckerConfig), 83 | typeof(INotifierConfig), 84 | }; 85 | 86 | public bool Accepts(Type type) 87 | { 88 | return CatchTypes.Contains(type); 89 | } 90 | 91 | public ExtensionConfigTypeConverter() 92 | { 93 | var deserializer = new DeserializerBuilder() 94 | .WithNamingConvention(HyphenatedNamingConvention.Instance) 95 | .IgnoreUnmatchedProperties() 96 | .Build(); 97 | 98 | _deserializer = deserializer; 99 | } 100 | 101 | public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) 102 | { 103 | var offset = parser.Current.Start.Index; 104 | var typeInterfaces = type.GetInterfaces(); 105 | 106 | var configTypeInterface = AvailableInterfaces 107 | .FirstOrDefault(x => typeInterfaces.Contains(x)); 108 | 109 | object obj = null; 110 | 111 | if (Mode == EctcMode.ConvertWithMap) 112 | { 113 | var yamlMark = YamlMarks.FirstOrDefault(x => x.AbsoluteOffset == offset 114 | && x.InterfaceType == configTypeInterface); 115 | 116 | var realConfigType = Map 117 | .FirstOrDefault(x => x.ConfigTypeInterface == yamlMark.InterfaceType 118 | && (x.ExtensionName is null || x.ExtensionName == yamlMark.ExtensionName)) 119 | .ImplType; 120 | 121 | obj = _deserializer.Deserialize(parser, realConfigType); 122 | } 123 | 124 | if (Mode == EctcMode.CollectYamlMarks) 125 | { 126 | obj = _deserializer.Deserialize(parser, type); 127 | var yamlMark = new ExtensionConfigYamlMark 128 | { 129 | ExtensionName = ((IExtensionConfig)obj).ExtensionName, 130 | AbsoluteOffset = (int)offset, 131 | InterfaceType = configTypeInterface 132 | }; 133 | 134 | YamlMarks.Add(yamlMark); 135 | } 136 | 137 | return obj; 138 | } 139 | 140 | public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) 141 | { 142 | throw new NotImplementedException(); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /.github/workflows/osx-x64.yml: -------------------------------------------------------------------------------- 1 | name: macOS-x64 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*-osx-x64' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: setup .net 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 9.0.x 18 | 19 | - name: restore dependencies 20 | run: dotnet restore src/Dhaf.sln 21 | 22 | # TODO: `--no-dependencies` using (this is difficult because extensions have their own directory, and they cannot detect the root `refs` directory) 23 | 24 | - name: build core 25 | run: dotnet publish src/Dhaf.Core/Dhaf.Core.csproj --configuration Release --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64/core /p:DebugType=None /p:DebugSymbols=false 26 | 27 | - name: build dhaf.node 28 | run: dotnet publish src/Dhaf.Node/Dhaf.Node.csproj --configuration Release --no-dependencies --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64 -p:PublishSingleFile=true --self-contained false /p:DebugType=None /p:DebugSymbols=false 29 | 30 | - name: build dhaf.cli 31 | run: dotnet publish src/Dhaf.CLI/Dhaf.CLI.csproj --configuration Release --no-dependencies --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64 -p:PublishSingleFile=true --self-contained false /p:DebugType=None /p:DebugSymbols=false 32 | 33 | - name: build core extensions 34 | run: | 35 | dotnet publish src/Dhaf.HealthCheckers.Web/Dhaf.HealthCheckers.Web.csproj --configuration Release --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64/ext/hc/web /p:DebugType=None /p:DebugSymbols=false 36 | dotnet publish src/Dhaf.HealthCheckers.Exec/Dhaf.HealthCheckers.Exec.csproj --configuration Release --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64/ext/hc/exec /p:DebugType=None /p:DebugSymbols=false 37 | dotnet publish src/Dhaf.HealthCheckers.Tcp/Dhaf.HealthCheckers.Tcp.csproj --configuration Release --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64/ext/hc/tcp /p:DebugType=None /p:DebugSymbols=false 38 | 39 | dotnet publish src/Dhaf.Switchers.Cloudflare/Dhaf.Switchers.Cloudflare.csproj --configuration Release --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64/ext/sw/cloudflare /p:DebugType=None /p:DebugSymbols=false 40 | dotnet publish src/Dhaf.Switchers.GoogleCloud/Dhaf.Switchers.GoogleCloud.csproj --configuration Release --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64/ext/sw/google-cloud /p:DebugType=None /p:DebugSymbols=false 41 | dotnet publish src/Dhaf.Switchers.Exec/Dhaf.Switchers.Exec.csproj --configuration Release --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64/ext/sw/exec /p:DebugType=None /p:DebugSymbols=false 42 | 43 | dotnet publish src/Dhaf.Notifiers.Email/Dhaf.Notifiers.Email.csproj --configuration Release --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64/ext/ntf/email /p:DebugType=None /p:DebugSymbols=false 44 | dotnet publish src/Dhaf.Notifiers.Telegram/Dhaf.Notifiers.Telegram.csproj --configuration Release --no-restore -nowarn:CS1998 -r osx-x64 -o bin/osx-x64/ext/ntf/tg /p:DebugType=None /p:DebugSymbols=false 45 | 46 | - name: set the release version from tag 47 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 48 | 49 | - name: prepare artifacts 50 | run: | 51 | mkdir -p dhaf-${{ env.RELEASE_VERSION }} 52 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/libs 53 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/web 54 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/exec 55 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/tcp 56 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/cloudflare 57 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/google-cloud 58 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/exec 59 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/email 60 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/tg 61 | 62 | mv ./bin/osx-x64/Dhaf.Node dhaf-${{ env.RELEASE_VERSION }}/dhaf.node 63 | mv ./bin/osx-x64/Dhaf.CLI dhaf-${{ env.RELEASE_VERSION }}/dhaf.cli 64 | mv ./bin/osx-x64/appsettings.json ./bin/osx-x64/nlog.config dhaf-${{ env.RELEASE_VERSION }} 65 | mv ./bin/osx-x64/core/* dhaf-${{ env.RELEASE_VERSION }}/libs 66 | 67 | mv ./bin/osx-x64/ext/hc/web/* dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/web 68 | mv ./bin/osx-x64/ext/hc/exec/* dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/exec 69 | mv ./bin/osx-x64/ext/hc/tcp/* dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/tcp 70 | mv ./bin/osx-x64/ext/sw/cloudflare/* dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/cloudflare 71 | mv ./bin/osx-x64/ext/sw/google-cloud/* dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/google-cloud 72 | mv ./bin/osx-x64/ext/sw/exec/* dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/exec 73 | mv ./bin/osx-x64/ext/ntf/email/* dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/email 74 | mv ./bin/osx-x64/ext/ntf/tg/* dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/tg 75 | 76 | - name: upload artifacts 77 | uses: actions/upload-artifact@master 78 | with: 79 | name: dhaf-${{ env.RELEASE_VERSION }} 80 | path: dhaf-${{ env.RELEASE_VERSION }} 81 | -------------------------------------------------------------------------------- /.github/workflows/win-x64.yml: -------------------------------------------------------------------------------- 1 | name: Windows-x64 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*-win-x64' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: setup .net 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 9.0.x 18 | 19 | - name: restore dependencies 20 | run: dotnet restore src/Dhaf.sln 21 | 22 | # TODO: `--no-dependencies` using (this is difficult because extensions have their own directory, and they cannot detect the root `refs` directory) 23 | 24 | - name: build core 25 | run: dotnet publish src/Dhaf.Core/Dhaf.Core.csproj --configuration Release --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64/core /p:DebugType=None /p:DebugSymbols=false 26 | 27 | - name: build dhaf.node 28 | run: dotnet publish src/Dhaf.Node/Dhaf.Node.csproj --configuration Release --no-dependencies --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64 -p:PublishSingleFile=true --self-contained false /p:DebugType=None /p:DebugSymbols=false 29 | 30 | - name: build dhaf.cli 31 | run: dotnet publish src/Dhaf.CLI/Dhaf.CLI.csproj --configuration Release --no-dependencies --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64 -p:PublishSingleFile=true --self-contained false /p:DebugType=None /p:DebugSymbols=false 32 | 33 | - name: build core extensions 34 | run: | 35 | dotnet publish src/Dhaf.HealthCheckers.Web/Dhaf.HealthCheckers.Web.csproj --configuration Release --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64/ext/hc/web /p:DebugType=None /p:DebugSymbols=false 36 | dotnet publish src/Dhaf.HealthCheckers.Exec/Dhaf.HealthCheckers.Exec.csproj --configuration Release --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64/ext/hc/exec /p:DebugType=None /p:DebugSymbols=false 37 | dotnet publish src/Dhaf.HealthCheckers.Tcp/Dhaf.HealthCheckers.Tcp.csproj --configuration Release --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64/ext/hc/tcp /p:DebugType=None /p:DebugSymbols=false 38 | 39 | dotnet publish src/Dhaf.Switchers.Cloudflare/Dhaf.Switchers.Cloudflare.csproj --configuration Release --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64/ext/sw/cloudflare /p:DebugType=None /p:DebugSymbols=false 40 | dotnet publish src/Dhaf.Switchers.GoogleCloud/Dhaf.Switchers.GoogleCloud.csproj --configuration Release --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64/ext/sw/google-cloud /p:DebugType=None /p:DebugSymbols=false 41 | dotnet publish src/Dhaf.Switchers.Exec/Dhaf.Switchers.Exec.csproj --configuration Release --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64/ext/sw/exec /p:DebugType=None /p:DebugSymbols=false 42 | 43 | dotnet publish src/Dhaf.Notifiers.Email/Dhaf.Notifiers.Email.csproj --configuration Release --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64/ext/ntf/email /p:DebugType=None /p:DebugSymbols=false 44 | dotnet publish src/Dhaf.Notifiers.Telegram/Dhaf.Notifiers.Telegram.csproj --configuration Release --no-restore -nowarn:CS1998 -r win-x64 -o bin/win-x64/ext/ntf/tg /p:DebugType=None /p:DebugSymbols=false 45 | 46 | - name: set the release version from tag 47 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 48 | 49 | - name: prepare artifacts 50 | run: | 51 | mkdir -p dhaf-${{ env.RELEASE_VERSION }} 52 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/libs 53 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/web 54 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/exec 55 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/tcp 56 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/cloudflare 57 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/google-cloud 58 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/exec 59 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/email 60 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/tg 61 | 62 | mv ./bin/win-x64/Dhaf.Node.exe dhaf-${{ env.RELEASE_VERSION }}/Dhaf.Node.exe 63 | mv ./bin/win-x64/Dhaf.CLI.exe dhaf-${{ env.RELEASE_VERSION }}/Dhaf.CLI.exe 64 | mv ./bin/win-x64/appsettings.json ./bin/win-x64/nlog.config dhaf-${{ env.RELEASE_VERSION }} 65 | mv ./bin/win-x64/core/* dhaf-${{ env.RELEASE_VERSION }}/libs 66 | 67 | mv ./bin/win-x64/ext/hc/web/* dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/web 68 | mv ./bin/win-x64/ext/hc/exec/* dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/exec 69 | mv ./bin/win-x64/ext/hc/tcp/* dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/tcp 70 | mv ./bin/win-x64/ext/sw/cloudflare/* dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/cloudflare 71 | mv ./bin/win-x64/ext/sw/google-cloud/* dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/google-cloud 72 | mv ./bin/win-x64/ext/sw/exec/* dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/exec 73 | mv ./bin/win-x64/ext/ntf/email/* dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/email 74 | mv ./bin/win-x64/ext/ntf/tg/* dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/tg 75 | 76 | - name: upload artifacts 77 | uses: actions/upload-artifact@master 78 | with: 79 | name: dhaf-${{ env.RELEASE_VERSION }} 80 | path: dhaf-${{ env.RELEASE_VERSION }} 81 | -------------------------------------------------------------------------------- /.github/workflows/linux-x64.yml: -------------------------------------------------------------------------------- 1 | name: Linux-x64 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*-linux-x64' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: setup .net 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 9.0.x 18 | 19 | - name: restore dependencies 20 | run: dotnet restore src/Dhaf.sln 21 | 22 | # TODO: `--no-dependencies` using (this is difficult because extensions have their own directory, and they cannot detect the root `refs` directory) 23 | 24 | - name: build core 25 | run: dotnet publish src/Dhaf.Core/Dhaf.Core.csproj --configuration Release --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64/core /p:DebugType=None /p:DebugSymbols=false 26 | 27 | - name: build dhaf.node 28 | run: dotnet publish src/Dhaf.Node/Dhaf.Node.csproj --configuration Release --no-dependencies --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64 -p:PublishSingleFile=true --self-contained false /p:DebugType=None /p:DebugSymbols=false 29 | 30 | - name: build dhaf.cli 31 | run: dotnet publish src/Dhaf.CLI/Dhaf.CLI.csproj --configuration Release --no-dependencies --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64 -p:PublishSingleFile=true --self-contained false /p:DebugType=None /p:DebugSymbols=false 32 | 33 | - name: build core extensions 34 | run: | 35 | dotnet publish src/Dhaf.HealthCheckers.Web/Dhaf.HealthCheckers.Web.csproj --configuration Release --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64/ext/hc/web /p:DebugType=None /p:DebugSymbols=false 36 | dotnet publish src/Dhaf.HealthCheckers.Exec/Dhaf.HealthCheckers.Exec.csproj --configuration Release --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64/ext/hc/exec /p:DebugType=None /p:DebugSymbols=false 37 | dotnet publish src/Dhaf.HealthCheckers.Tcp/Dhaf.HealthCheckers.Tcp.csproj --configuration Release --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64/ext/hc/tcp /p:DebugType=None /p:DebugSymbols=false 38 | 39 | dotnet publish src/Dhaf.Switchers.Cloudflare/Dhaf.Switchers.Cloudflare.csproj --configuration Release --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64/ext/sw/cloudflare /p:DebugType=None /p:DebugSymbols=false 40 | dotnet publish src/Dhaf.Switchers.GoogleCloud/Dhaf.Switchers.GoogleCloud.csproj --configuration Release --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64/ext/sw/google-cloud /p:DebugType=None /p:DebugSymbols=false 41 | dotnet publish src/Dhaf.Switchers.Exec/Dhaf.Switchers.Exec.csproj --configuration Release --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64/ext/sw/exec /p:DebugType=None /p:DebugSymbols=false 42 | 43 | dotnet publish src/Dhaf.Notifiers.Email/Dhaf.Notifiers.Email.csproj --configuration Release --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64/ext/ntf/email /p:DebugType=None /p:DebugSymbols=false 44 | dotnet publish src/Dhaf.Notifiers.Telegram/Dhaf.Notifiers.Telegram.csproj --configuration Release --no-restore -nowarn:CS1998 -r linux-x64 -o bin/linux-x64/ext/ntf/tg /p:DebugType=None /p:DebugSymbols=false 45 | 46 | - name: set the release version from tag 47 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 48 | 49 | - name: prepare artifacts 50 | run: | 51 | mkdir -p dhaf-${{ env.RELEASE_VERSION }} 52 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/libs 53 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/web 54 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/exec 55 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/tcp 56 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/cloudflare 57 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/google-cloud 58 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/exec 59 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/email 60 | mkdir -p dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/tg 61 | 62 | mv ./bin/linux-x64/Dhaf.Node dhaf-${{ env.RELEASE_VERSION }}/dhaf.node 63 | mv ./bin/linux-x64/Dhaf.CLI dhaf-${{ env.RELEASE_VERSION }}/dhaf.cli 64 | mv ./bin/linux-x64/appsettings.json ./bin/linux-x64/nlog.config dhaf-${{ env.RELEASE_VERSION }} 65 | mv ./bin/linux-x64/core/* dhaf-${{ env.RELEASE_VERSION }}/libs 66 | 67 | mv ./bin/linux-x64/ext/hc/web/* dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/web 68 | mv ./bin/linux-x64/ext/hc/exec/* dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/exec 69 | mv ./bin/linux-x64/ext/hc/tcp/* dhaf-${{ env.RELEASE_VERSION }}/extensions/health-checkers/tcp 70 | mv ./bin/linux-x64/ext/sw/cloudflare/* dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/cloudflare 71 | mv ./bin/linux-x64/ext/sw/google-cloud/* dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/google-cloud 72 | mv ./bin/linux-x64/ext/sw/exec/* dhaf-${{ env.RELEASE_VERSION }}/extensions/switchers/exec 73 | mv ./bin/linux-x64/ext/ntf/email/* dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/email 74 | mv ./bin/linux-x64/ext/ntf/tg/* dhaf-${{ env.RELEASE_VERSION }}/extensions/notifiers/tg 75 | 76 | - name: upload artifacts 77 | uses: actions/upload-artifact@master 78 | with: 79 | name: dhaf-${{ env.RELEASE_VERSION }} 80 | path: dhaf-${{ env.RELEASE_VERSION }} 81 | -------------------------------------------------------------------------------- /tests/Dhaf.Core.Tests/ClusterConfigParserTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Moq; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Dhaf.Core.Tests 8 | { 9 | public class ClusterConfigParserTest 10 | { 11 | [Fact] 12 | public async Task ParseTest() 13 | { 14 | var extensionsScope = GetExtensionsScope(); 15 | var _configuration = GetConfiguration(); 16 | 17 | var internalConfig = new DhafInternalConfig(); 18 | _configuration.Bind(internalConfig); 19 | 20 | var configParser = new ClusterConfigParser("Data/test_config_1.dhaf", extensionsScope, internalConfig); 21 | var parsedConfig = await configParser.Parse(); 22 | 23 | Assert.NotNull(parsedConfig.Dhaf); 24 | Assert.Equal("test-cr", parsedConfig.Dhaf.ClusterName); 25 | Assert.Equal("node-1", parsedConfig.Dhaf.NodeName); 26 | 27 | Assert.NotNull(parsedConfig.Dhaf.WebApi); 28 | Assert.Equal("localhost", parsedConfig.Dhaf.WebApi.Host); 29 | Assert.Equal(8128, parsedConfig.Dhaf.WebApi.Port); 30 | 31 | Assert.NotNull(parsedConfig.Etcd); 32 | Assert.Equal("http://11.22.33.44:2379", parsedConfig.Etcd.Hosts); 33 | 34 | Assert.NotNull(parsedConfig.Services); 35 | Assert.Equal(2, parsedConfig.Services.Count); 36 | 37 | var serv1 = parsedConfig.Services[0]; 38 | Assert.NotNull(serv1); 39 | Assert.Equal("serv1", serv1.Name); 40 | Assert.Equal("site.com", serv1.Domain); 41 | Assert.NotNull(serv1.EntryPoints); 42 | Assert.Equal(3, serv1.EntryPoints.Count); 43 | 44 | var serv1nc1 = serv1.EntryPoints[0]; 45 | Assert.Equal("nc1", serv1nc1.Id); 46 | Assert.Equal("100.1.1.1", serv1nc1.IP); 47 | 48 | var serv1nc2 = serv1.EntryPoints[1]; 49 | Assert.Equal("nc2", serv1nc2.Id); 50 | Assert.Equal("100.1.1.2", serv1nc2.IP); 51 | 52 | Assert.NotNull(serv1.Switcher); 53 | Assert.Equal("a", serv1.Switcher.ExtensionName); 54 | 55 | Assert.NotNull(serv1.HealthChecker); 56 | Assert.Equal("a", serv1.HealthChecker.ExtensionName); 57 | 58 | var serv2 = parsedConfig.Services[1]; 59 | Assert.NotNull(serv2); 60 | Assert.Equal("serv2", serv2.Name); 61 | Assert.Equal("foo.site.com", serv2.Domain); 62 | Assert.NotNull(serv2.EntryPoints); 63 | Assert.Equal(2, serv2.EntryPoints.Count); 64 | 65 | Assert.NotNull(parsedConfig.Notifiers); 66 | Assert.Single(parsedConfig.Notifiers); 67 | 68 | var ntf1 = parsedConfig.Notifiers[0]; 69 | Assert.NotNull(ntf1); 70 | Assert.Equal("a", ntf1.ExtensionName); 71 | Assert.Equal("a", ntf1.Name); 72 | } 73 | 74 | [Fact] 75 | public async Task ParseTest_Issue_25() 76 | { 77 | var extensionsScope = GetExtensionsScope(); 78 | var _configuration = GetConfiguration(); 79 | 80 | var internalConfig = new DhafInternalConfig(); 81 | _configuration.Bind(internalConfig); 82 | 83 | var configParser = new ClusterConfigParser("Data/test_config_issue_25.dhaf", extensionsScope, internalConfig); 84 | var parsedConfig = await configParser.Parse(); 85 | 86 | Assert.NotNull(parsedConfig.Dhaf.WebApi); 87 | Assert.Equal("localhost", parsedConfig.Dhaf.WebApi.Host); 88 | Assert.Equal(8128, parsedConfig.Dhaf.WebApi.Port); 89 | } 90 | 91 | private ExtensionsScope GetExtensionsScope() 92 | { 93 | var switcher = new Mock(); 94 | switcher.Setup(x => x.ExtensionName).Returns("cloudflare"); 95 | switcher.Setup(x => x.ConfigType).Returns(typeof(SwitcherConfigMock)); 96 | switcher.Setup(x => x.InternalConfigType).Returns(typeof(SwitcherInternalConfigMock)); 97 | 98 | var healthChecker = new Mock(); 99 | healthChecker.Setup(x => x.ExtensionName).Returns("web"); 100 | healthChecker.Setup(x => x.ConfigType).Returns(typeof(HealthCheckerConfigMock)); 101 | healthChecker.Setup(x => x.InternalConfigType).Returns(typeof(HealthCheckerInternalConfigMock)); 102 | 103 | var tgNotifier = new Mock(); 104 | tgNotifier.Setup(x => x.ExtensionName).Returns("tg"); 105 | tgNotifier.Setup(x => x.ConfigType).Returns(typeof(NotifierConfigMock)); 106 | tgNotifier.Setup(x => x.InternalConfigType).Returns(typeof(NotifierInternalConfigMock)); 107 | 108 | var extensionsScope = new ExtensionsScope 109 | { 110 | Switchers = new List>() 111 | { 112 | new DhafExtension 113 | { 114 | ExtensionPath = "sw/cloudflare", Instance = switcher.Object 115 | } 116 | }, 117 | HealthCheckers = new List>() 118 | { 119 | new DhafExtension 120 | { 121 | ExtensionPath = "hc/web", Instance = healthChecker.Object 122 | } 123 | }, 124 | Notifiers = new List>() 125 | { 126 | new DhafExtension 127 | { 128 | ExtensionPath = "ntf/tg", Instance = tgNotifier.Object 129 | } 130 | } 131 | }; 132 | 133 | return extensionsScope; 134 | } 135 | 136 | private IConfigurationRoot GetConfiguration() 137 | { 138 | // TODO: For performance reasons, it is better to do the configuration reading once before all the tests. 139 | // However, flexibility is lost in this way. 140 | 141 | return new ConfigurationBuilder() 142 | .AddJsonFile("appsettings.json") 143 | .Build(); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Dhaf.Notifiers.Telegram/UpdatesProcessing.cs: -------------------------------------------------------------------------------- 1 | using Dhaf.Core; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Telegram.Bot; 9 | using Telegram.Bot.Exceptions; 10 | using Telegram.Bot.Requests; 11 | using Telegram.Bot.Types; 12 | using Telegram.Bot.Types.Enums; 13 | 14 | namespace Dhaf.Notifiers.Telegram 15 | { 16 | public partial class TelegramNotifier : INotifier 17 | { 18 | private static readonly Update[] EmptyUpdates = Array.Empty(); 19 | private int? _messageOffset = null; 20 | private CancellationTokenSource _handleUpdatesWithIntervalCts = new(); 21 | private Task _handleUpdatesWithIntervalTask; 22 | 23 | protected async Task HandleUpdatesWithInterval(CancellationToken cancellationToken) 24 | { 25 | var interval = _internalConfig.UpdatesPollingInterval; 26 | 27 | while (!cancellationToken.IsCancellationRequested) 28 | { 29 | var updates = await ReceiveUpdates(); 30 | await HandleUpdates(updates); 31 | await Task.Delay(TimeSpan.FromSeconds(interval)); 32 | } 33 | } 34 | 35 | protected async Task HandleUpdates(Update[] updates) 36 | { 37 | foreach (var update in updates) 38 | { 39 | if (update.Message.Type == MessageType.LeftChatMember) 40 | { 41 | if (update.Message.LeftChatMember.Id == _botClient.BotId) 42 | { 43 | var chatId = update.Message.Chat.Id; 44 | await DeleteSubscriber(chatId); 45 | 46 | _logger.LogTrace($"{Sign} The bot was removed from chat with id <{chatId}>."); 47 | 48 | continue; 49 | } 50 | } 51 | 52 | if (update.Message.Type == MessageType.Text) 53 | { 54 | var chatId = update.Message.Chat.Id; 55 | 56 | try 57 | { 58 | if (update.Message.Text.StartsWith("/start")) 59 | { 60 | var subInStore = await GetSubscriberOfDefault(chatId); 61 | if (subInStore.HasValue) 62 | { 63 | await _botClient.SendTextMessageAsync( 64 | chatId: chatId, 65 | text: "You are already subscribed to notifications." 66 | ); 67 | 68 | continue; 69 | } 70 | 71 | var parts = update.Message.Text.Split(' '); 72 | if (parts.Length == 2) 73 | { 74 | var joinCode = parts[1]; 75 | if (joinCode == _config.JoinCode) 76 | { 77 | await PutSubscriber(chatId, update.Message.Chat.Type.ToString()); 78 | 79 | await _botClient.SendTextMessageAsync( 80 | chatId: chatId, 81 | text: "The join code is correct. You will now receive notifications from me." 82 | ); 83 | 84 | _logger.LogInformation($"{Sign} Chat with id <{chatId}> has been added to receive notifications."); 85 | } 86 | else 87 | { 88 | await _botClient.SendTextMessageAsync( 89 | chatId: chatId, 90 | text: "The join code is incorrect. Try again." 91 | ); 92 | } 93 | } 94 | else 95 | { 96 | await _botClient.SendTextMessageAsync( 97 | chatId: chatId, 98 | text: "Please use the following command format:\n```\n/start \n```", 99 | parseMode: ParseMode.MarkdownV2 100 | ); 101 | } 102 | } 103 | else 104 | { 105 | var subInStore = await GetSubscriberOfDefault(chatId); 106 | if (subInStore.HasValue) 107 | { 108 | await _botClient.SendTextMessageAsync( 109 | chatId: chatId, 110 | text: "I don't understand you. You're subscribed to notifications from me, so you can relax for now." 111 | ); 112 | } 113 | else 114 | { 115 | await _botClient.SendTextMessageAsync( 116 | chatId: chatId, 117 | text: "I don't understand you\\. By the way, you're not subscribed to notifications from me\\. I recommend that you do so using the following command:\n```\n/start \n```", 118 | parseMode: ParseMode.MarkdownV2 119 | ); 120 | } 121 | } 122 | } 123 | catch (ApiRequestException e) 124 | { 125 | await ProcessPossibleUnavailableSubscriber(e, chatId); 126 | } 127 | catch { } 128 | } 129 | } 130 | } 131 | 132 | protected async Task ReceiveUpdates() 133 | { 134 | var allowedUpdates = new List() 135 | { 136 | UpdateType.Message 137 | }; 138 | 139 | var updates = EmptyUpdates; 140 | 141 | var request = new GetUpdatesRequest 142 | { 143 | Offset = _messageOffset ?? 0, 144 | AllowedUpdates = allowedUpdates, 145 | }; 146 | 147 | try 148 | { 149 | updates = await _botClient.MakeRequestAsync(request); 150 | } 151 | catch { } 152 | 153 | if (updates.Any()) 154 | { 155 | var lastUpdateId = updates.Max(x => x.Id); 156 | _messageOffset = lastUpdateId + 1; 157 | } 158 | 159 | return updates; 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Dhaf.Core/ExtensionsScope/ExtensionsScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Text.Json; 6 | 7 | namespace Dhaf.Core 8 | { 9 | public class DhafExtension 10 | { 11 | public string ExtensionPath { get; set; } 12 | public T Instance { get; set; } 13 | } 14 | 15 | public class ExtensionsScope 16 | { 17 | public IEnumerable> HealthCheckers { get; set; } 18 | public IEnumerable> Switchers { get; set; } 19 | public IEnumerable> Notifiers { get; set; } 20 | } 21 | 22 | public class ExtensionMeta 23 | { 24 | public string EntryPoint { get; set; } 25 | 26 | public T InternalConfiguration { get; set; } 27 | } 28 | 29 | public class ExtensionsScopeFactory 30 | { 31 | public static T CreateSuchAs(T source) 32 | { 33 | var type = source.GetType(); 34 | var destination = Activator.CreateInstance(type); 35 | 36 | return (T)destination; 37 | } 38 | 39 | public static ExtensionsScope GetExtensionsScope(IEnumerable extensionsPath) 40 | { 41 | var healthCheckers = new List>(); 42 | var switchers = new List>(); 43 | var notifiers = new List>(); 44 | 45 | foreach (var path in extensionsPath) 46 | { 47 | var extAssembly = LoadExtension(path); 48 | var impls = GetImplementationsFromAssembly(extAssembly, path); 49 | 50 | healthCheckers.AddRange(impls.HealthCheckers); 51 | switchers.AddRange(impls.Switchers); 52 | notifiers.AddRange(impls.Notifiers); 53 | } 54 | 55 | return new ExtensionsScope 56 | { 57 | HealthCheckers = healthCheckers, 58 | Switchers = switchers, 59 | Notifiers = notifiers, 60 | }; 61 | } 62 | 63 | protected static Assembly LoadExtension(string path) 64 | { 65 | var extensionDir = $"extensions/{path}/"; 66 | var metaRaw = File.ReadAllText(extensionDir + "extension.json"); 67 | var meta = JsonSerializer.Deserialize>(metaRaw, DhafInternalConfig.JsonSerializerOptions); 68 | 69 | var loadContext = new ExtensionLoadContext(extensionDir + meta.EntryPoint); 70 | return loadContext.LoadFromAssemblyName( 71 | new AssemblyName(Path.GetFileNameWithoutExtension(extensionDir + meta.EntryPoint)) 72 | ); 73 | } 74 | 75 | protected static ExtensionsScope GetImplementationsFromAssembly(Assembly assembly, string extensionPath) 76 | { 77 | var healthCheckers = new List>(); 78 | var switchers = new List>(); 79 | var notifiers = new List>(); 80 | 81 | var types = assembly.GetTypes(); 82 | foreach (var type in types) 83 | { 84 | var healthChecker = GetImplementationOrDefault(type); 85 | if (healthChecker != null) 86 | { 87 | if (!typeof(IHealthCheckerConfig).IsAssignableFrom(healthChecker.ConfigType)) 88 | { 89 | throw new Exception($"The health checker config type <{healthChecker.ConfigType}> does not implement the dhaf health checker config interface."); 90 | } 91 | 92 | if (!typeof(IHealthCheckerInternalConfig).IsAssignableFrom(healthChecker.InternalConfigType)) 93 | { 94 | throw new Exception($"The health checker internal config type <{healthChecker.InternalConfigType}> does not implement the dhaf health checker internal interface."); 95 | } 96 | 97 | healthCheckers.Add(new DhafExtension 98 | { 99 | Instance = healthChecker, 100 | ExtensionPath = extensionPath 101 | }); 102 | 103 | continue; 104 | } 105 | 106 | var switcher = GetImplementationOrDefault(type); 107 | if (switcher != null) 108 | { 109 | if (!typeof(ISwitcherConfig).IsAssignableFrom(switcher.ConfigType)) 110 | { 111 | throw new Exception($"The switch config type <{switcher.ConfigType}> does not implement the dhaf switch config interface."); 112 | } 113 | 114 | if (!typeof(ISwitcherInternalConfig).IsAssignableFrom(switcher.InternalConfigType)) 115 | { 116 | throw new Exception($"The switch internal config type <{switcher.InternalConfigType}> does not implement the dhaf switch internal config interface."); 117 | } 118 | 119 | switchers.Add(new DhafExtension 120 | { 121 | Instance = switcher, 122 | ExtensionPath = extensionPath 123 | }); 124 | 125 | continue; 126 | } 127 | 128 | var notifier = GetImplementationOrDefault(type); 129 | if (notifier != null) 130 | { 131 | if (!typeof(INotifierConfig).IsAssignableFrom(notifier.ConfigType)) 132 | { 133 | throw new Exception($"The notifier config type <{notifier.ConfigType}> does not implement the dhaf notifier config interface."); 134 | } 135 | 136 | if (!typeof(INotifierInternalConfig).IsAssignableFrom(notifier.InternalConfigType)) 137 | { 138 | throw new Exception($"The notifier internal config type <{notifier.InternalConfigType}> does not implement the dhaf notifier internal config interface."); 139 | } 140 | 141 | notifiers.Add(new DhafExtension 142 | { 143 | Instance = notifier, 144 | ExtensionPath = extensionPath 145 | }); 146 | } 147 | } 148 | 149 | return new ExtensionsScope 150 | { 151 | HealthCheckers = healthCheckers, 152 | Switchers = switchers, 153 | Notifiers = notifiers 154 | }; 155 | } 156 | 157 | protected static T GetImplementationOrDefault(Type type) 158 | where T : class 159 | { 160 | if (typeof(T).IsAssignableFrom(type) 161 | && Activator.CreateInstance(type) is T impl) 162 | { 163 | return impl; 164 | } 165 | 166 | return null; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /project_identity/small.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << /Length 2 0 R >> 5 | stream 6 | 0.234000 0 0.000000 0.000000 0.234000 1.000000 d1 7 | 8 | endstream 9 | endobj 10 | 11 | 2 0 obj 12 | 50 13 | endobj 14 | 15 | 3 0 obj 16 | << /Length 4 0 R >> 17 | stream 18 | 0.493000 0 0.038000 0.000000 0.463000 0.897000 d1 19 | 20 | endstream 21 | endobj 22 | 23 | 4 0 obj 24 | 50 25 | endobj 26 | 27 | 5 0 obj 28 | << /Length 6 0 R >> 29 | stream 30 | 0.499000 0 0.038000 0.000000 0.461000 0.897000 d1 31 | 32 | endstream 33 | endobj 34 | 35 | 6 0 obj 36 | 50 37 | endobj 38 | 39 | 7 0 obj 40 | << /Length 8 0 R >> 41 | stream 42 | 0.399000 0 0.038000 0.000000 0.382000 0.897000 d1 43 | 44 | endstream 45 | endobj 46 | 47 | 8 0 obj 48 | 50 49 | endobj 50 | 51 | 9 0 obj 52 | << /Length 10 0 R >> 53 | stream 54 | 0.485000 0 0.015000 0.000000 0.471000 0.874000 d1 55 | 56 | endstream 57 | endobj 58 | 59 | 10 0 obj 60 | 50 61 | endobj 62 | 63 | 11 0 obj 64 | [ 0.234000 0.499000 0.399000 0.493000 0.485000 ] 65 | endobj 66 | 67 | 12 0 obj 68 | << /Length 13 0 R >> 69 | stream 70 | /CIDInit /ProcSet findresource begin 71 | 12 dict begin 72 | begincmap 73 | /CIDSystemInfo 74 | << /Registry (FigmaPDF) 75 | /Ordering (FigmaPDF) 76 | /Supplement 0 77 | >> def 78 | /CMapName /A-B-C def 79 | /CMapType 2 def 80 | 1 begincodespacerange 81 | <00> 82 | endcodespacerange 83 | 1 beginbfchar 84 | <00> <0020> 85 | endbfchar 86 | 1 beginbfchar 87 | <01> <0048> 88 | endbfchar 89 | 1 beginbfchar 90 | <02> <0046> 91 | endbfchar 92 | 1 beginbfchar 93 | <03> <0044> 94 | endbfchar 95 | 1 beginbfchar 96 | <04> <0041> 97 | endbfchar 98 | endcmap 99 | CMapName currentdict /CMap defineresource pop 100 | end 101 | end 102 | endstream 103 | endobj 104 | 105 | 13 0 obj 106 | 476 107 | endobj 108 | 109 | 14 0 obj 110 | << /Subtype /Type3 111 | /CharProcs << /C0 1 0 R 112 | /C3 3 0 R 113 | /C1 5 0 R 114 | /C2 7 0 R 115 | /C4 9 0 R 116 | >> 117 | /Encoding << /Type /Encoding 118 | /Differences [ 0 /C0 /C1 /C2 /C3 /C4 ] 119 | >> 120 | /Widths 11 0 R 121 | /FontBBox [ 0.000000 0.000000 0.000000 0.000000 ] 122 | /FontMatrix [ 1.000000 0.000000 0.000000 1.000000 0.000000 0.000000 ] 123 | /Type /Font 124 | /ToUnicode 12 0 R 125 | /FirstChar 0 126 | /LastChar 4 127 | /Resources << >> 128 | >> 129 | endobj 130 | 131 | 15 0 obj 132 | << /Font << /F1 14 0 R >> >> 133 | endobj 134 | 135 | 16 0 obj 136 | << /Length 17 0 R >> 137 | stream 138 | /DeviceRGB CS 139 | /DeviceRGB cs 140 | q 141 | 1.000000 0.000000 -0.000000 1.000000 1282.177979 1781.162109 cm 142 | 0.145833 0.145833 0.145833 scn 143 | 123.656822 224.682129 m 144 | 146.705200 0.000061 l 145 | 0.000000 0.000061 l 146 | 123.656822 224.682129 l 147 | h 148 | f 149 | n 150 | Q 151 | q 152 | 1.000000 0.000000 -0.000000 1.000000 1282.177979 1781.162109 cm 153 | 0.145833 0.145833 0.145833 scn 154 | 283.193970 224.682114 m 155 | 260.145630 0.000061 l 156 | 410.399628 0.000061 l 157 | 283.193970 224.682114 l 158 | h 159 | f 160 | n 161 | Q 162 | q 163 | 1.000000 0.000000 -0.000000 1.000000 181.972412 1093.675293 cm 164 | 0.145833 0.145833 0.145833 scn 165 | 196.375854 -250.899902 m 166 | h 167 | 230.575851 522.200134 m 168 | 443.875854 522.200134 l 169 | 499.075836 522.200134 540.475830 506.900116 568.075867 476.300110 c 170 | 595.675903 445.700104 609.775879 400.400116 610.375854 340.400116 c 171 | 612.175842 -21.399902 l 172 | 612.775879 -97.599915 599.575867 -154.899902 572.575867 -193.299927 c 173 | 545.575867 -231.699890 500.875854 -250.899902 438.475861 -250.899902 c 174 | 230.575851 -250.899902 l 175 | 230.575851 522.200134 l 176 | h 177 | 413.275879 -114.099915 m 178 | 441.475861 -114.099915 455.575867 -100.299927 455.575867 -72.699890 c 179 | 455.575867 326.000122 l 180 | 455.575867 343.400116 454.075867 356.300110 451.075867 364.700104 c 181 | 448.675873 373.700134 443.875854 379.700165 436.675873 382.700134 c 182 | 429.475891 385.700134 418.375885 387.200134 403.375854 387.200134 c 183 | 386.275879 387.200134 l 184 | 386.275879 -114.099915 l 185 | 413.275879 -114.099915 l 186 | h 187 | 640.223511 -250.899902 m 188 | h 189 | 674.423523 -250.899902 m 190 | 674.423523 522.200134 l 191 | 828.323486 522.200134 l 192 | 828.323486 245.900146 l 193 | 901.223511 245.900146 l 194 | 901.223511 522.200134 l 195 | 1055.123535 522.200134 l 196 | 1055.123535 -250.899902 l 197 | 901.223511 -250.899902 l 198 | 901.223511 100.100098 l 199 | 828.323486 100.100098 l 200 | 828.323486 -250.899902 l 201 | 674.423523 -250.899902 l 202 | h 203 | 1511.219604 -250.899902 m 204 | h 205 | 1545.419556 -250.899902 m 206 | 1545.419556 522.200134 l 207 | 1855.019653 522.200134 l 208 | 1855.019653 371.900116 l 209 | 1701.119629 371.900116 l 210 | 1701.119629 248.600098 l 211 | 1846.919678 248.600098 l 212 | 1846.919678 100.100098 l 213 | 1701.119629 100.100098 l 214 | 1701.119629 -250.899902 l 215 | 1545.419556 -250.899902 l 216 | h 217 | f 218 | n 219 | Q 220 | q 221 | 1.000000 0.000000 -0.000000 1.000000 181.972412 1093.675293 cm 222 | BT 223 | 900.000000 0.000000 0.000000 900.000000 196.375854 -250.899902 Tm 224 | /F1 1.000000 Tf 225 | [ (\003) (\001) (\000) (\000) (\002) ] TJ 226 | ET 227 | Q 228 | q 229 | -1.000000 0.000000 -0.000000 -1.000000 2198.093994 1489.029785 cm 230 | 0.145833 0.145833 0.145833 scn 231 | 494.591797 -250.899902 m 232 | h 233 | 508.091797 -250.899902 m 234 | 582.791809 522.200134 l 235 | 844.691833 522.200134 l 236 | 918.491821 -250.899902 l 237 | 771.791809 -250.899902 l 238 | 760.991821 -125.799866 l 239 | 667.391785 -125.799866 l 240 | 658.391785 -250.899902 l 241 | 508.091797 -250.899902 l 242 | h 243 | 679.091797 -2.499878 m 244 | 749.291809 -2.499878 l 245 | 715.091797 390.800140 l 246 | 707.891785 390.800140 l 247 | 679.091797 -2.499878 l 248 | h 249 | f 250 | n 251 | Q 252 | q 253 | -1.000000 0.000000 -0.000000 -1.000000 2198.093994 1489.029785 cm 254 | BT 255 | 900.000000 0.000000 0.000000 900.000000 494.591797 -250.899902 Tm 256 | /F1 1.000000 Tf 257 | [ (\004) ] TJ 258 | ET 259 | Q 260 | 261 | endstream 262 | endobj 263 | 264 | 17 0 obj 265 | 2849 266 | endobj 267 | 268 | 18 0 obj 269 | << /Annots [] 270 | /Type /Page 271 | /MediaBox [ 0.000000 0.000000 2444.000000 2444.000000 ] 272 | /Resources 15 0 R 273 | /Contents 16 0 R 274 | /Parent 19 0 R 275 | >> 276 | endobj 277 | 278 | 19 0 obj 279 | << /Kids [ 18 0 R ] 280 | /Count 1 281 | /Type /Pages 282 | >> 283 | endobj 284 | 285 | 20 0 obj 286 | << /Type /Catalog 287 | /Pages 19 0 R 288 | >> 289 | endobj 290 | 291 | xref 292 | 0 21 293 | 0000000000 65535 f 294 | 0000000010 00000 n 295 | 0000000116 00000 n 296 | 0000000137 00000 n 297 | 0000000243 00000 n 298 | 0000000264 00000 n 299 | 0000000370 00000 n 300 | 0000000391 00000 n 301 | 0000000497 00000 n 302 | 0000000518 00000 n 303 | 0000000625 00000 n 304 | 0000000647 00000 n 305 | 0000000715 00000 n 306 | 0000001249 00000 n 307 | 0000001272 00000 n 308 | 0000001835 00000 n 309 | 0000001883 00000 n 310 | 0000004790 00000 n 311 | 0000004814 00000 n 312 | 0000004995 00000 n 313 | 0000005071 00000 n 314 | trailer 315 | << /ID [ (some) (id) ] 316 | /Root 20 0 R 317 | /Size 21 318 | >> 319 | startxref 320 | 5132 321 | %%EOF -------------------------------------------------------------------------------- /src/Dhaf.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31005.135 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Node", "Dhaf.Node\Dhaf.Node.csproj", "{8567AF9C-6406-431F-9C1D-74460B9CBC4A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Switchers.Cloudflare", "Dhaf.Switchers.Cloudflare\Dhaf.Switchers.Cloudflare.csproj", "{53A14176-6048-4CD6-B13E-C46AE119FE7B}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.CLI", "Dhaf.CLI\Dhaf.CLI.csproj", "{BE053C9E-8668-4746-A283-02CA32EE552E}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Switchers.Exec", "Dhaf.Switchers.Exec\Dhaf.Switchers.Exec.csproj", "{AF16D4AF-96D2-4D0A-B057-1FD5BC888E4B}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.HealthCheckers.Web", "Dhaf.HealthCheckers.Web\Dhaf.HealthCheckers.Web.csproj", "{E34EFAB0-FAC0-4444-B981-42C60A2762B4}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Core", "Dhaf.Core\Dhaf.Core.csproj", "{ACE75F82-05C9-4BE6-A528-12F7CF4ABFDE}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.HealthCheckers.Exec", "Dhaf.HealthCheckers.Exec\Dhaf.HealthCheckers.Exec.csproj", "{F8C98E17-835F-41F5-AB7E-5CE85AD010E6}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Notifiers.Email", "Dhaf.Notifiers.Email\Dhaf.Notifiers.Email.csproj", "{01665C8E-8D27-4CDB-A965-2756D8030624}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Notifiers.Telegram", "Dhaf.Notifiers.Telegram\Dhaf.Notifiers.Telegram.csproj", "{010BAE78-C9AD-4F60-A3AD-7588C724E52A}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Core.Tests", "..\tests\Dhaf.Core.Tests\Dhaf.Core.Tests.csproj", "{B24910B3-1629-4C77-81B6-CED348589E29}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Node.DataTransferObjects", "Dhaf.Node.DataTransferObjects\Dhaf.Node.DataTransferObjects.csproj", "{7BCA415F-B775-428C-8903-26FCDC1AFCB3}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dhaf.Switchers.GoogleCloud", "Dhaf.Switchers.GoogleCloud\Dhaf.Switchers.GoogleCloud.csproj", "{B90E1B46-8259-4B93-85FB-D7C50F09E582}" 29 | EndProject 30 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dhaf.HealthCheckers.Tcp", "Dhaf.HealthCheckers.Tcp\Dhaf.HealthCheckers.Tcp.csproj", "{60712C16-B67F-485D-A64A-0E446EAD4EFB}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {8567AF9C-6406-431F-9C1D-74460B9CBC4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {8567AF9C-6406-431F-9C1D-74460B9CBC4A}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {8567AF9C-6406-431F-9C1D-74460B9CBC4A}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {8567AF9C-6406-431F-9C1D-74460B9CBC4A}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {53A14176-6048-4CD6-B13E-C46AE119FE7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {53A14176-6048-4CD6-B13E-C46AE119FE7B}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {53A14176-6048-4CD6-B13E-C46AE119FE7B}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {53A14176-6048-4CD6-B13E-C46AE119FE7B}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {BE053C9E-8668-4746-A283-02CA32EE552E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {BE053C9E-8668-4746-A283-02CA32EE552E}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {BE053C9E-8668-4746-A283-02CA32EE552E}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {BE053C9E-8668-4746-A283-02CA32EE552E}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {AF16D4AF-96D2-4D0A-B057-1FD5BC888E4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {AF16D4AF-96D2-4D0A-B057-1FD5BC888E4B}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {AF16D4AF-96D2-4D0A-B057-1FD5BC888E4B}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {AF16D4AF-96D2-4D0A-B057-1FD5BC888E4B}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {E34EFAB0-FAC0-4444-B981-42C60A2762B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {E34EFAB0-FAC0-4444-B981-42C60A2762B4}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {E34EFAB0-FAC0-4444-B981-42C60A2762B4}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {E34EFAB0-FAC0-4444-B981-42C60A2762B4}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {ACE75F82-05C9-4BE6-A528-12F7CF4ABFDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {ACE75F82-05C9-4BE6-A528-12F7CF4ABFDE}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {ACE75F82-05C9-4BE6-A528-12F7CF4ABFDE}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {ACE75F82-05C9-4BE6-A528-12F7CF4ABFDE}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {F8C98E17-835F-41F5-AB7E-5CE85AD010E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {F8C98E17-835F-41F5-AB7E-5CE85AD010E6}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {F8C98E17-835F-41F5-AB7E-5CE85AD010E6}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {F8C98E17-835F-41F5-AB7E-5CE85AD010E6}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {01665C8E-8D27-4CDB-A965-2756D8030624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {01665C8E-8D27-4CDB-A965-2756D8030624}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {01665C8E-8D27-4CDB-A965-2756D8030624}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {01665C8E-8D27-4CDB-A965-2756D8030624}.Release|Any CPU.Build.0 = Release|Any CPU 70 | {010BAE78-C9AD-4F60-A3AD-7588C724E52A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 71 | {010BAE78-C9AD-4F60-A3AD-7588C724E52A}.Debug|Any CPU.Build.0 = Debug|Any CPU 72 | {010BAE78-C9AD-4F60-A3AD-7588C724E52A}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {010BAE78-C9AD-4F60-A3AD-7588C724E52A}.Release|Any CPU.Build.0 = Release|Any CPU 74 | {B24910B3-1629-4C77-81B6-CED348589E29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 75 | {B24910B3-1629-4C77-81B6-CED348589E29}.Debug|Any CPU.Build.0 = Debug|Any CPU 76 | {B24910B3-1629-4C77-81B6-CED348589E29}.Release|Any CPU.ActiveCfg = Release|Any CPU 77 | {B24910B3-1629-4C77-81B6-CED348589E29}.Release|Any CPU.Build.0 = Release|Any CPU 78 | {7BCA415F-B775-428C-8903-26FCDC1AFCB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 79 | {7BCA415F-B775-428C-8903-26FCDC1AFCB3}.Debug|Any CPU.Build.0 = Debug|Any CPU 80 | {7BCA415F-B775-428C-8903-26FCDC1AFCB3}.Release|Any CPU.ActiveCfg = Release|Any CPU 81 | {7BCA415F-B775-428C-8903-26FCDC1AFCB3}.Release|Any CPU.Build.0 = Release|Any CPU 82 | {B90E1B46-8259-4B93-85FB-D7C50F09E582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 83 | {B90E1B46-8259-4B93-85FB-D7C50F09E582}.Debug|Any CPU.Build.0 = Debug|Any CPU 84 | {B90E1B46-8259-4B93-85FB-D7C50F09E582}.Release|Any CPU.ActiveCfg = Release|Any CPU 85 | {B90E1B46-8259-4B93-85FB-D7C50F09E582}.Release|Any CPU.Build.0 = Release|Any CPU 86 | {60712C16-B67F-485D-A64A-0E446EAD4EFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 87 | {60712C16-B67F-485D-A64A-0E446EAD4EFB}.Debug|Any CPU.Build.0 = Debug|Any CPU 88 | {60712C16-B67F-485D-A64A-0E446EAD4EFB}.Release|Any CPU.ActiveCfg = Release|Any CPU 89 | {60712C16-B67F-485D-A64A-0E446EAD4EFB}.Release|Any CPU.Build.0 = Release|Any CPU 90 | EndGlobalSection 91 | GlobalSection(SolutionProperties) = preSolution 92 | HideSolutionNode = FALSE 93 | EndGlobalSection 94 | GlobalSection(ExtensibilityGlobals) = postSolution 95 | SolutionGuid = {CB278208-B774-42D8-AD9A-33944C91F157} 96 | EndGlobalSection 97 | EndGlobal 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_h.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *_wpftmp.csproj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush personal settings 296 | .cr/personal 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | 333 | # Local History for Visual Studio 334 | .localhistory/ 335 | 336 | # Launch Settings for Visual Studio 337 | **/*launchSettings.json 338 | 339 | # Temp files 340 | /tmp/* 341 | 342 | # 343 | templates/dhaf_extensions/libs/*.* 344 | 345 | # macOS files 346 | .DS_Store 347 | --------------------------------------------------------------------------------