Skip to content

Commit 4a4a5de

Browse files
committed
Check Kubernetes cluster endpoint for defining the ssl_ca_cert (#5143)
This is a follow up in the Kubernetes service implementation for running untrusted jobs. The current implementation was working for dev environment because the K8s cluster is set there with default configurations. During the rollout for the external project, the cluster needed to be created with security rules, and one of them is having a private ip, and set the DNS to resolve both internal and external ips. As the endpoint for this cluster is a name (defined by the dns) and not a ip, we shouldn't set the ssl certificate, as the authentication will be managed by the DNS. This PR updates the load credentials function to check the cluster endpoint and only set the ssl cert if the endpoint is a ip. Signed-off-by: Javan Lacerda <javanlacerda@google.com>
1 parent 2d35b4c commit 4a4a5de

2 files changed

Lines changed: 107 additions & 1 deletion

File tree

src/clusterfuzz/_internal/k8s/service.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""Kubernetes batch client."""
1515
import base64
1616
import collections
17+
import ipaddress
1718
import os
1819
import typing
1920
import uuid
@@ -206,7 +207,16 @@ def _load_gke_credentials(self):
206207

207208
configuration = k8s_client.Configuration()
208209
configuration.host = f'https://{endpoint}'
209-
configuration.ssl_ca_cert = ca_cert_path
210+
211+
try:
212+
ipaddress.ip_address(endpoint)
213+
configuration.ssl_ca_cert = ca_cert_path
214+
except ValueError:
215+
# If the endpoint is a hostname, we assume it's using a public CA or
216+
# the system trust store should be used.
217+
logs.info(f'Endpoint {endpoint} is a hostname. '
218+
'Skipping custom CA configuration.')
219+
210220
configuration.verify_ssl = True
211221

212222
def get_token(creds):
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Tests for KubernetesService credential loading."""
15+
16+
import os
17+
import unittest
18+
from unittest import mock
19+
20+
from clusterfuzz._internal.k8s import service
21+
from clusterfuzz._internal.tests.test_libs import helpers
22+
23+
24+
class KubernetesCredentialsTest(unittest.TestCase):
25+
"""Tests for KubernetesService credential loading."""
26+
27+
def setUp(self):
28+
helpers.patch(self, [
29+
'clusterfuzz._internal.system.environment.get_value',
30+
'google.auth.default',
31+
'googleapiclient.discovery.build',
32+
'kubernetes.client.Configuration',
33+
'kubernetes.config.load_kube_config',
34+
])
35+
self.mock.get_value.return_value = 'test-project'
36+
creds = mock.Mock()
37+
creds.token = 'test-token'
38+
self.mock.default.return_value = (creds, 'test-project')
39+
40+
self.mock_discovery = self.mock.build.return_value
41+
self.mock_clusters = self.mock_discovery.projects.return_value.locations.return_value.clusters.return_value
42+
43+
self.mock_config_instance = self.mock.Configuration.return_value
44+
45+
os.environ['BOT_DIR'] = '/tmp'
46+
47+
def test_load_gke_credentials_ip_endpoint(self):
48+
"""Test loading credentials with an IP endpoint (should set ssl_ca_cert)."""
49+
self.mock_clusters.list.return_value.execute.return_value = {
50+
'clusters': [{
51+
'name': 'clusterfuzz-cronjobs-gke',
52+
'endpoint': '1.2.3.4',
53+
'masterAuth': {
54+
'clusterCaCertificate': 'dGVzdA==' # base64 "test"
55+
}
56+
}]
57+
}
58+
59+
# Bypass __init__ logic to call _load_gke_credentials directly
60+
with mock.patch.object(
61+
service.KubernetesService, '__init__', return_value=None):
62+
kube_service = service.KubernetesService()
63+
64+
# pylint: disable=protected-access
65+
kube_service._load_gke_credentials()
66+
67+
self.assertEqual(self.mock_config_instance.host, 'https://1.2.3.4')
68+
self.assertIsNotNone(self.mock_config_instance.ssl_ca_cert)
69+
self.assertTrue(self.mock_config_instance.verify_ssl)
70+
71+
def test_load_gke_credentials_hostname_endpoint(self):
72+
"""Test loading credentials with a hostname endpoint (should skip ssl_ca_cert)."""
73+
self.mock_clusters.list.return_value.execute.return_value = {
74+
'clusters': [{
75+
'name': 'clusterfuzz-cronjobs-gke',
76+
'endpoint': 'example.com',
77+
'masterAuth': {
78+
'clusterCaCertificate': 'dGVzdA=='
79+
}
80+
}]
81+
}
82+
83+
# Bypass __init__ logic to call _load_gke_credentials directly
84+
with mock.patch.object(
85+
service.KubernetesService, '__init__', return_value=None):
86+
kube_service = service.KubernetesService()
87+
88+
# Reset mock to ensure we capture the specific call
89+
self.mock_config_instance.ssl_ca_cert = None
90+
91+
# pylint: disable=protected-access
92+
kube_service._load_gke_credentials()
93+
94+
self.assertEqual(self.mock_config_instance.host, 'https://example.com')
95+
self.assertIsNone(self.mock_config_instance.ssl_ca_cert)
96+
self.assertTrue(self.mock_config_instance.verify_ssl)

0 commit comments

Comments
 (0)