using EntKube.Clusters.Features.AdoptCluster;
namespace EntKube.Clusters.Domain;
///
/// 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.
///
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 PrometheusEndpoints => prometheusEndpoints.AsReadOnly();
public IReadOnlyList Components => components.AsReadOnly();
public IReadOnlyList StorageBuckets => storageBuckets.AsReadOnly();
private List prometheusEndpoints = new();
private List components = new();
private List storageBuckets = new();
private KubernetesCluster() { }
///
/// 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.
///
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
};
}
///
/// After a successful health check, we mark the cluster as connected.
/// This means the platform can now schedule work against this cluster.
///
public void MarkConnected()
{
Status = ClusterStatus.Connected;
LastHealthCheckAt = DateTimeOffset.UtcNow;
}
///
/// 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.
///
public void MarkUnreachable()
{
Status = ClusterStatus.Unreachable;
LastHealthCheckAt = DateTimeOffset.UtcNow;
}
///
/// 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".
///
public void AssignToEnvironment(Guid environmentId)
{
if (environmentId == Guid.Empty)
{
throw new ArgumentException("Environment ID is required.", nameof(environmentId));
}
EnvironmentId = environmentId;
}
///
/// 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.
///
public void SetPrometheusEndpoints(List endpoints)
{
prometheusEndpoints = endpoints ?? new List();
}
///
/// 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.
///
public void UpdateComponents(List checkResults)
{
components = checkResults
.Select(ClusterComponent.FromCheckResult)
.ToList();
}
///
/// 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.
///
public void UpdateComponentConfiguration(string componentName, Dictionary values)
{
ClusterComponent? component = components.FirstOrDefault(
c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
component?.MergeConfiguration(values);
}
///
/// Marks a tracked component as Installed after a successful deployment.
/// If the component hasn't been tracked yet, this is a no-op.
///
public void MarkComponentInstalled(string componentName)
{
ClusterComponent? component = components.FirstOrDefault(
c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
component?.MarkInstalled();
}
///
/// Updates the version of a tracked component after a successful upgrade.
/// If the component hasn't been tracked yet, this is a no-op.
///
public void UpdateComponentVersion(string componentName, string version)
{
ClusterComponent? component = components.FirstOrDefault(
c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
component?.UpdateVersion(version);
}
///
/// Marks a tracked component as NotInstalled after a successful uninstall.
/// If the component hasn't been tracked yet, this is a no-op.
///
public void MarkComponentUninstalled(string componentName)
{
ClusterComponent? component = components.FirstOrDefault(
c => c.ComponentName.Equals(componentName, StringComparison.OrdinalIgnoreCase));
component?.MarkUninstalled();
}
///
/// 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.
///
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;
}
///
/// 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.
///
public void AddStorageBucket(StorageBucket bucket)
{
storageBuckets.Add(bucket);
}
///
/// Removes a storage bucket from this cluster's tracked buckets.
/// Called after the bucket has been deleted from the S3 backend.
///
public void RemoveStorageBucket(Guid bucketId)
{
storageBuckets.RemoveAll(b => b.Id == bucketId);
}
}
///
/// The cloud provider hosting a Kubernetes cluster. Determines which additional
/// APIs are available for management (OpenStack, Gardener, billing, etc.).
///
public enum CloudProvider
{
None,
Cleura,
Aws,
Azure,
Gcp
}
///
/// 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.
///
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
}