public async Task ResolveConnectionDetails()

in src/library/Utilities/RemoteContainerConnectionDetailsResolver.cs [41:203]


        public async Task<RemoteContainerConnectionDetails> ResolveConnectionDetails(
            RemoteContainerConnectionDetails remoteContainerConnectionDetails,
            CancellationToken cancellationToken)
        {
            // If we are in prep there is no need to resolve anything since we are only using the remoteContainerConnectionDetails to hold a namespace
            // TODO (lolodi): this needs to be refactored so that we don't need to use such workarounds.
            if (remoteContainerConnectionDetails.AgentHostingMode == RemoteAgentHostingMode.PrepConnect)
            {
                return remoteContainerConnectionDetails;
            }

            // Check if restoration pod is present and in running state. This is an indication that previous session is still connected or it has not finished restoring yet.
            var allPods = (await _kubernetesClient.ListPodsInNamespaceAsync(remoteContainerConnectionDetails.NamespaceName, null, cancellationToken))?.Items;
            if (allPods != null && allPods.Count > 0) {
                var podsRunningRestorationJob = allPods.Where(p => StringComparer.OrdinalIgnoreCase.Equals(p.Status?.Phase, "Running") &&
                                                        p.Metadata.Name.StartsWith(remoteContainerConnectionDetails.ServiceName + "-restore") &&
                                                        (p.Status?.ContainerStatuses?.Where(cs => cs.Image.Contains(ImageProvider.DevHostRestorationJob.Name)).Any() ?? false));
                if (podsRunningRestorationJob.Count() > 0)
                {
                    _log.Warning("Restoration image {0} was found for '{1}' service. This means previous session is not restored yet. Try again once previous session is disconnected and restored.",
                    ImageProvider.DevHostRestorationJob.Name, remoteContainerConnectionDetails.ServiceName);
                    throw new UserVisibleException(_operationContext, Resources.PreviousSessionStillConnected, remoteContainerConnectionDetails.ServiceName);
                }
            }

            // Our target in remoteContainerConnectionDetails can be specified in a variety of ways. The most common one is Service.
            // If a service is specified we need to find the pod(s) that back that service and, if routing is disabled, the deployment that back the pods.
            // The deployment is not used when routing is enabled because routing deploys a new standalone pod.

            // If we are targeting a Service, we need to resolve the pod (or pods) that back the service.
            if (remoteContainerConnectionDetails.SourceEntityType == KubernetesEntityType.Service)
            {
                var service = await _kubernetesClient.GetV1ServiceAsync(
                    remoteContainerConnectionDetails.NamespaceName,
                    remoteContainerConnectionDetails.ServiceName,
                    cancellationToken);
                remoteContainerConnectionDetails.UpdateServiceDetails(service ?? throw new UserVisibleException(_operationContext, Resources.SpecifiedServiceCouldNotBeFoundFormat, remoteContainerConnectionDetails.ServiceName, remoteContainerConnectionDetails.NamespaceName));

                var pods = await this._GetTargetPodsFromServiceAsync(
                    remoteContainerConnectionDetails.NamespaceName,
                    remoteContainerConnectionDetails.Service,
                    cancellationToken);

                _log.Verbose($"Resolved {pods.Count()} from service.");

                // We want to see how many containers each pod has. Our current assumption is that all pods backing a service have the same number of containers, but if this is not the case
                // we will need to either improve how we resolve the pod, or provide a way for the user to tell us which container to use
                var minNumberContainers = pods.Select(p => p.Spec.Containers.Count).Min();
                var maxNumberContainers = pods.Select(p => p.Spec.Containers.Count).Max();
                _log.Verbose($"Max number of containers in pod: {maxNumberContainers}, min number of containers in pod: {minNumberContainers}");

                // If there are multiple pods backing the service it is probably because there is a deployment or statefulset with replicas > 1
                // To be sure we check if the pods are backed by the same deployment and while we are at it we cache the deployment in case we need it later.
                if (pods.Count() > 1)
                {
                    await ResolveBackingObjectDetailsAsync(remoteContainerConnectionDetails, pods, cancellationToken);
                }

                // We now have the pod(s). We need to update the remoteContainerConnectionDetails with it.
                // TODO: This system of setting the source entity multiple times (e.g. we start as "Service", get set to "Pod" and then later to "Deployment"/"StatefulSet")
                // is confusing and hard to follow. We should find a better way to do this while still reusing logic between the different types.
                var firstPod = pods.First();
                _log.Verbose($"Chose pod '{new PII(firstPod?.Name())}' with {firstPod.Spec.Containers.Count} containers");
                remoteContainerConnectionDetails.UpdateSourceEntityTypeToPod(firstPod);

                V1Container container;
                // If a containerName was specified let's make sure it exists
                if (!string.IsNullOrWhiteSpace(remoteContainerConnectionDetails.ContainerName))
                {
                    _log.Verbose($"Container {new PII(remoteContainerConnectionDetails.ContainerName)} was specified.");
                    container = remoteContainerConnectionDetails.Pod.Spec.Containers.Where(c => StringComparer.OrdinalIgnoreCase.Equals(c.Name, remoteContainerConnectionDetails.ContainerName)).FirstOrDefault();
                    remoteContainerConnectionDetails.UpdateContainerDetails(container ?? throw new UserVisibleException(_operationContext, Resources.SpecifiedContainerNotFoundFormat, remoteContainerConnectionDetails.ContainerName, remoteContainerConnectionDetails.PodName));
                }
                else
                {
                    // The user did not specify a container so we need to infere it from the pod and service
                    container = (this.GetContainerFromPodAndService(remoteContainerConnectionDetails.Pod, remoteContainerConnectionDetails.Service)) ??
                                    throw new UserVisibleException(_operationContext, Resources.FailedToIDContainerFormat, remoteContainerConnectionDetails.Pod.Metadata.Name);
                }
                remoteContainerConnectionDetails.UpdateContainerDetails(container);
            }

            // If we are targeting a Deployment, we need to fetch it from K8S and make sure the container is there
            if (remoteContainerConnectionDetails.SourceEntityType == KubernetesEntityType.Deployment)
            {
                var deployment = await _kubernetesClient.GetV1DeploymentAsync(
                    remoteContainerConnectionDetails.NamespaceName,
                    remoteContainerConnectionDetails.DeploymentName,
                    cancellationToken);
                remoteContainerConnectionDetails.UpdateDeploymentDetails(deployment ?? throw new UserVisibleException(_operationContext, Resources.FailedToFindDeploymentFormat, remoteContainerConnectionDetails.DeploymentName, remoteContainerConnectionDetails.NamespaceName));

                // If a containerName was specified let's make sure it exists
                if (!string.IsNullOrWhiteSpace(remoteContainerConnectionDetails.ContainerName))
                {
                    var container = remoteContainerConnectionDetails.Deployment.Spec.Template.Spec.Containers.Where(c => StringComparer.OrdinalIgnoreCase.Equals(c.Name, remoteContainerConnectionDetails.ContainerName)).FirstOrDefault();
                    remoteContainerConnectionDetails.UpdateContainerDetails(container ?? throw new UserVisibleException(_operationContext, Resources.FailedToFindContainerInDeploymentFormat, remoteContainerConnectionDetails.DeploymentName, remoteContainerConnectionDetails.NamespaceName));
                }
                else
                {
                    var container = this.GetContainerFromDeployment(remoteContainerConnectionDetails.Deployment);
                    remoteContainerConnectionDetails.UpdateContainerDetails(container ?? throw new UserVisibleException(_operationContext, Resources.FailedToFindContainerInDeploymentFormat, remoteContainerConnectionDetails.DeploymentName, remoteContainerConnectionDetails.NamespaceName));
                }
            }

            // If we are targeting a Pod, it might be that is beacuse we were targeting a Service, and we moved to a Pod, or because the user specified a Pod manually (only the Pod name is known)
            if (remoteContainerConnectionDetails.SourceEntityType == KubernetesEntityType.Pod)
            {
                if (remoteContainerConnectionDetails.Pod == null)
                {
                    // We have a prefix of the pod name. We try to look for the pod based on what we have.
                    _log.Verbose($"Trying to resolve pod that matches {new PII(remoteContainerConnectionDetails.PodName)}...");
                    var pods = await _kubernetesClient.ListPodsInNamespaceAsync(remoteContainerConnectionDetails.NamespaceName, cancellationToken: cancellationToken);
                    var pod = pods.Items.FirstOrDefault(p => p.Metadata.Name.StartsWith(remoteContainerConnectionDetails.PodName));
                    remoteContainerConnectionDetails.UpdatePodDetails(pod ?? throw new UserVisibleException(_operationContext, Resources.FailedToFindPodInNamespaceFormat, remoteContainerConnectionDetails.PodName, remoteContainerConnectionDetails.NamespaceName));
                }
                // We know have the full pod, or because it was resolved from the service before, or because we just resolved it above.

                if (remoteContainerConnectionDetails.Container == null)
                {
                    // If a containerName was specified let's make sure it exists
                    if (!string.IsNullOrWhiteSpace(remoteContainerConnectionDetails.ContainerName))
                    {
                        var container = remoteContainerConnectionDetails.Pod.Spec.Containers.Where(c => StringComparer.OrdinalIgnoreCase.Equals(c.Name, remoteContainerConnectionDetails.ContainerName)).FirstOrDefault();
                        remoteContainerConnectionDetails.UpdateContainerDetails(container ?? throw new UserVisibleException(_operationContext, Resources.SpecifiedContainerNotFoundFormat, remoteContainerConnectionDetails.ContainerName, remoteContainerConnectionDetails.PodName));
                    }
                    else
                    {
                        var container = this.GetContainerFromPod(remoteContainerConnectionDetails.Pod);
                        remoteContainerConnectionDetails.UpdateContainerDetails(container ?? throw new UserVisibleException(_operationContext, Resources.FailedToFindContainerInPodFormat, remoteContainerConnectionDetails.ContainerName, remoteContainerConnectionDetails.PodName));
                    }
                }
            }

            // If source entity is a Pod, and routing is OFF, try to find the deployment that is hosting the Pod
            if (remoteContainerConnectionDetails.SourceEntityType == KubernetesEntityType.Pod &&
            remoteContainerConnectionDetails.AgentHostingMode == RemoteAgentHostingMode.Replace)
            {
                try
                {
                    // We might already have a backing object here if we came in as Service and then found multiple Pods backed by the same Deployment or Statefulset
                    if (remoteContainerConnectionDetails.Deployment == null && remoteContainerConnectionDetails.StatefulSet == null)
                    {
                        await ResolveBackingObjectDetailsAsync(remoteContainerConnectionDetails, remoteContainerConnectionDetails.Pod.AsEnumerable(), cancellationToken);
                    }
                    // The container at this point should already have been resolved when we processed the Pod in the block above, so no need to re-resolve it.

                    if (remoteContainerConnectionDetails.Deployment != null)
                    {
                        remoteContainerConnectionDetails.UpdateSourceEntityTypeToDeployment(remoteContainerConnectionDetails.Deployment);
                    }
                    else if (remoteContainerConnectionDetails.StatefulSet != null)
                    {
                        remoteContainerConnectionDetails.UpdateSourceEntityTypeToStatefulSet(remoteContainerConnectionDetails.StatefulSet);
                    }
                }
                catch (Exception e)
                {
                    _log.Info($"Failed to resolve backing object for pod: {e.Message}. Proceeding to debug single pod.");
                }
            }

            return remoteContainerConnectionDetails;
        }