-
Notifications
You must be signed in to change notification settings - Fork 355
[cherry-pick] prevent dynamic service deletion during upgrade #16153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
| import io.kubernetes.client.openapi.apis.CoreV1Api; | ||
| import io.kubernetes.client.openapi.models.V1ObjectMeta; | ||
| import io.kubernetes.client.openapi.models.V1OwnerReference; | ||
| import io.kubernetes.client.openapi.models.V1Pod; | ||
| import io.kubernetes.client.openapi.models.V1Service; | ||
| import io.kubernetes.client.openapi.models.V1ServiceBuilder; | ||
| import io.kubernetes.client.openapi.models.V1ServiceList; | ||
|
|
@@ -34,10 +35,10 @@ | |
| import java.io.IOException; | ||
| import java.net.HttpURLConnection; | ||
| import java.net.InetSocketAddress; | ||
| import java.util.ArrayList; | ||
| import java.util.Base64; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
@@ -78,6 +79,7 @@ | |
| private static final String SERVICE_TYPE_CLUSTER_IP = "ClusterIP"; | ||
| private static final String PAYLOAD_NAME = "cdap.service.payload"; | ||
|
|
||
| private final String podName; | ||
| private final String namespace; | ||
| private final String namePrefix; | ||
| private final Map<String, DefaultServiceDiscovered> serviceDiscovereds; | ||
|
|
@@ -97,26 +99,24 @@ | |
| private final Map<String, String> loadBalancerServiceAnnotations; | ||
|
|
||
| /** | ||
| * Constructor to create an instance for service discovery on the given | ||
| * Kubernetes namespace. | ||
| * Constructor to create an instance for service discovery. | ||
| * | ||
| * @param namespace the Kubernetes namespace to perform service discovery on | ||
| * @param namePrefix prefix applies to all service names in k8s | ||
| * @param namespace the Kubernetes namespace to perform service discovery on | ||
| * @param namePrefix prefix applies to all service names in k8s | ||
| * @param podName name of the current pod | ||
| * @param podLabels labels of the current pod | ||
| * @param ownerReferences owner references to set on created services | ||
| * @param apiClientFactory factory to create Kubernetes API clients | ||
| * @param loadBalancerServiceList list of services that should be exposed via LoadBalancer | ||
| * @param loadBalancerServiceAnnotations annotations to apply to LoadBalancer services | ||
| */ | ||
| public KubeDiscoveryService(String namespace, String namePrefix, | ||
| Map<String, String> podLabels, | ||
| List<V1OwnerReference> ownerReferences, | ||
| ApiClientFactory apiClientFactory) { | ||
| this(namespace, namePrefix, podLabels, ownerReferences, apiClientFactory, | ||
| new ArrayList<>(), new HashMap<>()); | ||
| } | ||
|
|
||
| public KubeDiscoveryService(String namespace, String namePrefix, | ||
| public KubeDiscoveryService(String namespace, String namePrefix, String podName, | ||
|
Check warning on line 113 in cdap-kubernetes/src/main/java/io/cdap/cdap/k8s/discovery/KubeDiscoveryService.java
|
||
| Map<String, String> podLabels, List<V1OwnerReference> ownerReferences, | ||
| ApiClientFactory apiClientFactory, List<String> loadBalancerServiceList, | ||
| Map<String, String> loadBalancerServiceAnnotations) { | ||
| this.namespace = namespace; | ||
| this.namePrefix = namePrefix; | ||
| this.podName = podName; | ||
| this.serviceDiscovereds = new ConcurrentHashMap<>(); | ||
| this.apiClientFactory = apiClientFactory; | ||
| this.podLabels = podLabels; | ||
|
|
@@ -134,6 +134,13 @@ | |
|
|
||
| try { | ||
| CoreV1Api api = getCoreApi(); | ||
| if (isPodTerminating(api)) { | ||
| LOG.info("Pod {} is terminating. Skipping service registration for {}.", podName, | ||
| discoverable.getName()); | ||
| return () -> { | ||
| }; | ||
| } | ||
|
|
||
| while (true) { | ||
| Optional<V1Service> currentService = getV1Service(api, serviceName, | ||
| discoverable.getName()); | ||
|
|
@@ -352,6 +359,9 @@ | |
| if (port == null || port != discoverable.getSocketAddress().getPort()) { | ||
| return true; | ||
| } | ||
| if (hasOwnerReferencesChanged(currentService, ownerReferences)) { | ||
| return true; | ||
| } | ||
| // If service type is Cluster IP no need to update. We don't check if the | ||
| // service's Cluster IP has changed because the discoverable stores only the | ||
| // service's hostname. We rely on Kube DNS to resolve the hostname. Kube DNS | ||
|
|
@@ -371,6 +381,29 @@ | |
| return true; | ||
| } | ||
|
|
||
| private boolean hasOwnerReferencesChanged(V1Service currentService, | ||
| List<V1OwnerReference> expectedOwners) { | ||
| List<V1OwnerReference> currentOwners = Optional.ofNullable(currentService.getMetadata()) | ||
| .map(V1ObjectMeta::getOwnerReferences).orElse(Collections.emptyList()); | ||
|
|
||
| return !new HashSet<>(currentOwners).equals(new HashSet<>(expectedOwners)); | ||
| } | ||
|
|
||
| private boolean isPodTerminating(CoreV1Api api) throws ApiException { | ||
| try { | ||
| V1Pod pod = api.readNamespacedPod(podName, namespace, null); | ||
| return Optional.ofNullable(pod.getMetadata()).map(V1ObjectMeta::getDeletionTimestamp) | ||
| .isPresent(); | ||
|
|
||
| } catch (ApiException e) { | ||
| // If it is a 404 ApiException, the pod is already gone. Treat as terminating. | ||
| if (e.getCode() == HttpURLConnection.HTTP_NOT_FOUND) { | ||
| return true; | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
|
Comment on lines
+392
to
+405
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the Kubernetes service account lacks permissions to read pods (e.g., due to RBAC restrictions returning 403 Forbidden) or if there is a transient API error, private boolean isPodTerminating(CoreV1Api api) {
if (podName == null) {
return false;
}
try {
V1Pod pod = api.readNamespacedPod(podName, namespace, null);
return Optional.ofNullable(pod.getMetadata()).map(V1ObjectMeta::getDeletionTimestamp)
.isPresent();
} catch (ApiException e) {
// If it is a 404 ApiException, the pod is already gone. Treat as terminating.
if (e.getCode() == HttpURLConnection.HTTP_NOT_FOUND) {
return true;
}
LOG.warn("Failed to read pod status for {}. Assuming pod is not terminating.", podName, e);
return false;
}
} |
||
|
|
||
| // Returns the k8s service type for the discoverable. | ||
| private String getServiceType(Discoverable discoverable) { | ||
| if (loadBalancerServiceList.contains(discoverable.getName())) { | ||
|
|
@@ -563,7 +596,6 @@ | |
| payload); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * A {@link Thread} that keep watching for changes in service in Kubernetes. | ||
| */ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
expectedOwnersparameter is annotated with@Nullable, but passing anullvalue tonew HashSet<>(expectedOwners)will throw aNullPointerException. We should safely handle the case whereexpectedOwnersis null by defaulting to an empty list.