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 }