275 lines
10 KiB
C#
275 lines
10 KiB
C#
using EntKube.Clusters.Features.AdoptCluster;
|
|
|
|
namespace EntKube.Clusters.Domain;
|
|
|
|
/// <summary>
|
|
/// A KubernetesCluster represents a registered cluster that EntKube manages.
|
|
/// It holds the connection details, health state, and metadata needed to
|
|
/// interact with the cluster's API server. Every cluster belongs to exactly
|
|
/// one tenant — no cluster can exist without an owning tenant. The kubeconfig
|
|
/// is stored as the credential material for API access.
|
|
/// </summary>
|
|
public class KubernetesCluster
|
|
{
|
|
public Guid Id { get; private set; }
|
|
public Guid TenantId { get; private set; }
|
|
public Guid EnvironmentId { get; private set; }
|
|
public string Name { get; private set; } = string.Empty;
|
|
public string ApiServerUrl { get; private set; } = string.Empty;
|
|
public string ContextName { get; private set; } = string.Empty;
|
|
public ClusterStatus Status { get; private set; }
|
|
public CloudProvider Provider { get; private set; }
|
|
public ProviderCredentials? ProviderCredentials { get; private set; }
|
|
public string KubeConfig { get; private set; } = string.Empty;
|
|
public DateTimeOffset RegisteredAt { get; private set; }
|
|
public DateTimeOffset? LastHealthCheckAt { get; private set; }
|
|
public IReadOnlyList<PrometheusEndpoint> PrometheusEndpoints => prometheusEndpoints.AsReadOnly();
|
|
public IReadOnlyList<ClusterComponent> Components => components.AsReadOnly();
|
|
public IReadOnlyList<StorageBucket> StorageBuckets => storageBuckets.AsReadOnly();
|
|
|
|
private List<PrometheusEndpoint> prometheusEndpoints = new();
|
|
private List<ClusterComponent> components = new();
|
|
private List<StorageBucket> storageBuckets = new();
|
|
|
|
private KubernetesCluster() { }
|
|
|
|
/// <summary>
|
|
/// Registers a new cluster in the platform. The caller must provide a tenant,
|
|
/// cluster name, API server URL, kubeconfig, and the environment it belongs to.
|
|
/// The cluster starts in a Pending state until a health check confirms it's reachable.
|
|
/// Every cluster must be assigned to an environment — no orphan clusters allowed.
|
|
/// </summary>
|
|
public static KubernetesCluster Register(string name, string apiServerUrl, string kubeConfig, Guid tenantId, string contextName, Guid environmentId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
throw new ArgumentException("Cluster name is required.", nameof(name));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(apiServerUrl))
|
|
{
|
|
throw new ArgumentException("API server URL is required.", nameof(apiServerUrl));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(kubeConfig))
|
|
{
|
|
throw new ArgumentException("KubeConfig is required.", nameof(kubeConfig));
|
|
}
|
|
|
|
if (tenantId == Guid.Empty)
|
|
{
|
|
throw new ArgumentException("A tenant must be specified.", nameof(tenantId));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(contextName))
|
|
{
|
|
throw new ArgumentException("A context name must be specified.", nameof(contextName));
|
|
}
|
|
|
|
if (environmentId == Guid.Empty)
|
|
{
|
|
throw new ArgumentException("An environment must be specified.", nameof(environmentId));
|
|
}
|
|
|
|
return new KubernetesCluster
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TenantId = tenantId,
|
|
EnvironmentId = environmentId,
|
|
Name = name,
|
|
ApiServerUrl = apiServerUrl,
|
|
ContextName = contextName,
|
|
KubeConfig = kubeConfig,
|
|
Status = ClusterStatus.Pending,
|
|
RegisteredAt = DateTimeOffset.UtcNow
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// After a successful health check, we mark the cluster as connected.
|
|
/// This means the platform can now schedule work against this cluster.
|
|
/// </summary>
|
|
public void MarkConnected()
|
|
{
|
|
Status = ClusterStatus.Connected;
|
|
LastHealthCheckAt = DateTimeOffset.UtcNow;
|
|
}
|
|
|
|
/// <summary>
|
|
/// When a health check fails, we mark the cluster as unreachable.
|
|
/// Existing workloads keep running, but no new provisioning can happen
|
|
/// until connectivity is restored.
|
|
/// </summary>
|
|
public void MarkUnreachable()
|
|
{
|
|
Status = ClusterStatus.Unreachable;
|
|
LastHealthCheckAt = DateTimeOffset.UtcNow;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns (or reassigns) this cluster to an environment. A cluster lives
|
|
/// in exactly one environment at a time — for example dev01 belongs to "dev",
|
|
/// test01 and test02 belong to "test", prod01 belongs to "prod".
|
|
/// </summary>
|
|
public void AssignToEnvironment(Guid environmentId)
|
|
{
|
|
if (environmentId == Guid.Empty)
|
|
{
|
|
throw new ArgumentException("Environment ID is required.", nameof(environmentId));
|
|
}
|
|
|
|
EnvironmentId = environmentId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replaces all discovered Prometheus endpoints with a fresh scan result.
|
|
/// Called after the platform scans the cluster for Prometheus services.
|
|
/// If multiple instances are found, they are all stored so metrics can
|
|
/// be aggregated from all of them.
|
|
/// </summary>
|
|
public void SetPrometheusEndpoints(List<PrometheusEndpoint> endpoints)
|
|
{
|
|
prometheusEndpoints = endpoints ?? new List<PrometheusEndpoint>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replaces all tracked components with a fresh scan result from the adoption
|
|
/// check. Each component check result is converted into a ClusterComponent
|
|
/// that persists the detected state — status, version, namespace, config.
|
|
/// This is called after every adoption scan so stored state stays current.
|
|
/// </summary>
|
|
public void UpdateComponents(List<ComponentCheckResult> checkResults)
|
|
{
|
|
components = checkResults
|
|
.Select(ClusterComponent.FromCheckResult)
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merges new configuration values into an already-tracked component.
|
|
/// Called after a successful configure operation so the persisted config
|
|
/// reflects what's actually deployed. If the component hasn't been
|
|
/// tracked yet (no prior scan), this is a no-op.
|
|
/// </summary>
|
|
public void UpdateComponentConfiguration(string componentName, Dictionary<string, string> values)
|
|
{
|
|
ClusterComponent? component = components.FirstOrDefault(
|
|
c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
component?.MergeConfiguration(values);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Marks a tracked component as Installed after a successful deployment.
|
|
/// If the component hasn't been tracked yet, this is a no-op.
|
|
/// </summary>
|
|
public void MarkComponentInstalled(string componentName)
|
|
{
|
|
ClusterComponent? component = components.FirstOrDefault(
|
|
c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
component?.MarkInstalled();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the version of a tracked component after a successful upgrade.
|
|
/// If the component hasn't been tracked yet, this is a no-op.
|
|
/// </summary>
|
|
public void UpdateComponentVersion(string componentName, string version)
|
|
{
|
|
ClusterComponent? component = components.FirstOrDefault(
|
|
c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
component?.UpdateVersion(version);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Marks a tracked component as NotInstalled after a successful uninstall.
|
|
/// If the component hasn't been tracked yet, this is a no-op.
|
|
/// </summary>
|
|
public void MarkComponentUninstalled(string componentName)
|
|
{
|
|
ClusterComponent? component = components.FirstOrDefault(
|
|
c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
component?.MarkUninstalled();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns a cloud provider to this cluster. Providers like Cleura offer
|
|
/// additional APIs (OpenStack, Gardener) that EntKube can leverage for
|
|
/// provisioning, billing, and resource management. Credentials are required
|
|
/// for any provider other than None.
|
|
/// </summary>
|
|
public void SetProvider(CloudProvider provider, ProviderCredentials? credentials)
|
|
{
|
|
if (provider != CloudProvider.None && credentials is null)
|
|
{
|
|
throw new ArgumentException(
|
|
$"Credentials are required when setting provider to {provider}.",
|
|
nameof(credentials));
|
|
}
|
|
|
|
Provider = provider;
|
|
ProviderCredentials = provider == CloudProvider.None ? null : credentials;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a newly created storage bucket on this cluster. The bucket
|
|
/// has already been created on the S3 backend — this persists the metadata
|
|
/// so the platform knows about it and components can reference it.
|
|
/// </summary>
|
|
public void AddStorageBucket(StorageBucket bucket)
|
|
{
|
|
storageBuckets.Add(bucket);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a storage bucket from this cluster's tracked buckets.
|
|
/// Called after the bucket has been deleted from the S3 backend.
|
|
/// </summary>
|
|
public void RemoveStorageBucket(Guid bucketId)
|
|
{
|
|
storageBuckets.RemoveAll(b => b.Id == bucketId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The cloud provider hosting a Kubernetes cluster. Determines which additional
|
|
/// APIs are available for management (OpenStack, Gardener, billing, etc.).
|
|
/// </summary>
|
|
public enum CloudProvider
|
|
{
|
|
None,
|
|
Cleura,
|
|
Aws,
|
|
Azure,
|
|
Gcp
|
|
}
|
|
|
|
/// <summary>
|
|
/// Credentials for authenticating with a cluster's cloud provider APIs.
|
|
/// Cleura exposes two separate APIs:
|
|
/// 1. Cleura Cloud REST API (rest.cleura.cloud) — uses Cleura login credentials
|
|
/// for account management, Gardener K8s cluster ops, billing, domain listing.
|
|
/// 2. OpenStack API (e.g. sto2.citycloud.com:5000) — uses OpenStack/Keystone
|
|
/// credentials for infrastructure: compute, networking, storage, load balancing.
|
|
/// Both credential sets are stored here; either may be null if not configured.
|
|
/// </summary>
|
|
public record ProviderCredentials(
|
|
string Username,
|
|
string Password,
|
|
string Region,
|
|
string? OpenStackAuthUrl = null,
|
|
string? OpenStackProjectId = null,
|
|
string? OpenStackUsername = null,
|
|
string? OpenStackPassword = null,
|
|
string? OpenStackUserDomainName = null);
|
|
|
|
public enum ClusterStatus
|
|
{
|
|
Pending,
|
|
Connected,
|
|
Unreachable
|
|
}
|