Skip to content

Commit 61e795a

Browse files
authored
fix(auth): exit early when agent cert config is outside well-known directory (#17762)
To prevent unnecessary startup latency, this updates the agent certificate retrieval to only poll when the config points to the well-known workload directory (where Cloud Run dynamically mounts files). For all other paths, it exits early. - Extracts config parsing into a dedicated helper function - Removes the hardcoded fallback to the well-known path when no config is provided - Adds test coverage for new logic fixes b/522957573
1 parent 81482ba commit 61e795a

2 files changed

Lines changed: 293 additions & 170 deletions

File tree

packages/google-auth/google/auth/_agent_identity_utils.py

Lines changed: 90 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -67,93 +67,88 @@ def _is_certificate_file_ready(path):
6767
st = os.stat(path)
6868
return stat.S_ISREG(st.st_mode) and st.st_size > 0
6969
except PermissionError:
70-
# Propagate PermissionError to let caller handle it (fail-fast or fallback)
70+
# Propagate PermissionError to let caller handle it (e.g., return early or fallback)
7171
raise
7272
except OSError:
7373
return False
7474

7575

7676
def get_agent_identity_certificate_path():
77-
"""Gets the certificate path from the certificate config file.
77+
"""Gets the agent certificate path from the certificate config file.
7878
7979
The path to the certificate config file is read from the
8080
GOOGLE_API_CERTIFICATE_CONFIG environment variable. This function
81-
implements a retry mechanism to handle cases where the environment
81+
can optionally trigger polling to handle cases where the environment
8282
variable is set before the files are available on the filesystem.
8383
8484
Returns:
85-
str: The path to the leaf certificate file.
85+
Optional[str]: The path to the agent's certificate file, or None if unavailable.
8686
8787
Raises:
8888
google.auth.exceptions.RefreshError: If the certificate config file
8989
or the certificate file cannot be found after retries.
9090
"""
91-
import json
92-
9391
cert_config_path = os.environ.get(environment_vars.GOOGLE_API_CERTIFICATE_CONFIG)
9492

95-
# Check if the well-known workload directory is mounted.
93+
if not cert_config_path:
94+
return None
95+
96+
# We trigger polling only if the config path points to the well-known directory.
97+
# Cloud Run dynamically generates these files in this directory, and both the
98+
# config file and the certificate file may experience a brief startup latency.
99+
# For all other paths, we return early to avoid introducing unnecessary startup
100+
# delays.
96101
well_known_dir = os.path.dirname(_WELL_KNOWN_CERT_PATH)
97-
has_well_known_dir = os.path.exists(well_known_dir)
102+
try:
103+
abs_cert_path = os.path.abspath(cert_config_path)
104+
abs_well_known_dir = os.path.abspath(well_known_dir)
105+
should_poll = (
106+
os.path.commonpath([abs_well_known_dir, abs_cert_path])
107+
== abs_well_known_dir
108+
)
109+
except ValueError:
110+
should_poll = False
98111

99-
# If we have neither a config path nor a well-known mount directory, exit immediately.
100-
if not cert_config_path and not has_well_known_dir:
101-
return None
112+
return _get_cert_path_with_optional_polling(cert_config_path, should_poll)
102113

103-
# If ECP config path is specified but does not exist, and we are on a workstation, fail-fast immediately.
104-
if (
105-
cert_config_path
106-
and not has_well_known_dir
107-
and not os.path.exists(cert_config_path)
108-
):
109-
return None
110114

115+
def _get_cert_path_with_optional_polling(cert_config_path, should_poll):
116+
"""Gets the certificate path, optionally polling until it is ready.
117+
118+
Args:
119+
cert_config_path (str): The path to the certificate configuration file.
120+
should_poll (bool): If True, the function will poll for the file and
121+
certificate to be ready. If False, it will check only once and
122+
return early if they are not immediately available.
123+
124+
Returns:
125+
str: The path to the certificate file, or None if unavailable.
126+
127+
Raises:
128+
google.auth.exceptions.RefreshError: If the certificate config file
129+
or the certificate file cannot be found after retries.
130+
"""
111131
has_logged_config_warning = False
112132
has_logged_cert_warning = False
113133

114134
for interval in _POLLING_INTERVALS:
115135
try:
116-
# Path A: Config file is explicitly set
117-
if cert_config_path:
118-
with open(cert_config_path, "r") as f:
119-
cert_config = json.load(f)
120-
121-
cert_configs = (
122-
cert_config.get("cert_configs")
123-
if isinstance(cert_config, dict)
124-
else None
125-
)
126-
workload_config = (
127-
cert_configs.get("workload")
128-
if isinstance(cert_configs, dict)
129-
else None
130-
)
131-
132-
if (
133-
not isinstance(workload_config, dict)
134-
or "cert_path" not in workload_config
135-
):
136-
return None
137-
138-
cert_path = workload_config["cert_path"]
139-
if _is_certificate_file_ready(cert_path):
140-
return cert_path
136+
cert_path = _parse_cert_path_from_config(cert_config_path)
141137

142-
# The config was parsed, but the cert file is not ready yet
143-
target_path = cert_path
138+
if cert_path is None:
139+
return None
144140

145-
# Path B: Config is NOT set, fallback to the well-known path
146-
else:
147-
if _is_certificate_file_ready(_WELL_KNOWN_CERT_PATH):
148-
return _WELL_KNOWN_CERT_PATH
141+
if _is_certificate_file_ready(cert_path):
142+
return cert_path
149143

150-
# The well-known cert file is not ready yet
151-
target_path = _WELL_KNOWN_CERT_PATH
144+
# The config was parsed, but the cert file is not ready yet
145+
if not should_poll:
146+
# If polling is disabled, return early.
147+
return None
152148

153-
# Log a warning on the first failed attempt to load the certificate file
154149
if not has_logged_cert_warning:
155150
warnings.warn(
156-
f"Certificate file not ready at {target_path}. Retrying until startup timeout (up to {_TOTAL_TIMEOUT} seconds total)..."
151+
f"Certificate file not ready at {cert_path}. Retrying until startup timeout (up to {_TOTAL_TIMEOUT} seconds total)..."
157152
)
158153
has_logged_cert_warning = True
159154

@@ -164,25 +159,24 @@ def get_agent_identity_certificate_path():
164159
)
165160
return None
166161
except (IOError, ValueError, KeyError) as e:
167-
if cert_config_path and os.path.exists(cert_config_path):
162+
if os.path.exists(cert_config_path):
168163
# If the file exists but has invalid JSON or is unreadable,
169-
# we assume it is in its final format and fail-fast by returning None.
164+
# we assume it is in its final format and return early (returning None).
165+
return None
166+
167+
if not should_poll:
168+
# If polling is disabled, return early if the file doesn't exist.
170169
return None
171170

172-
if not has_logged_config_warning and cert_config_path:
171+
if not has_logged_config_warning:
173172
warnings.warn(
174173
f"Certificate config file not found or incomplete: {e} (from "
175174
f"{environment_vars.GOOGLE_API_CERTIFICATE_CONFIG} environment variable). "
176175
f"Retrying until startup timeout (up to {_TOTAL_TIMEOUT} seconds total)..."
177176
)
178177
has_logged_config_warning = True
179-
pass
180178

181-
# A sleep is required in two cases:
182-
# 1. The config file is not found (the except block).
183-
# 2. The config file/well-known path is found, but the certificate is not yet available.
184-
# In both cases, we need to poll, so we sleep on every iteration
185-
# that doesn't return a certificate.
179+
# Sleep before the next polling attempt.
186180
time.sleep(interval)
187181

188182
raise exceptions.RefreshError(
@@ -193,6 +187,40 @@ def get_agent_identity_certificate_path():
193187
)
194188

195189

190+
def _parse_cert_path_from_config(cert_config_path):
191+
"""Reads the cert config file and returns the cert_path.
192+
193+
Args:
194+
cert_config_path (str): The path to the certificate configuration file.
195+
196+
Returns:
197+
Optional[str]: The path to the certificate file, or None if not found
198+
in the config.
199+
200+
Raises:
201+
IOError: If the certificate config file cannot be read.
202+
ValueError: If the certificate config file contains invalid JSON.
203+
KeyError: If the certificate config file does not contain the
204+
expected structure.
205+
"""
206+
import json
207+
208+
with open(cert_config_path, "r", encoding="utf-8") as f:
209+
cert_config = json.load(f)
210+
211+
cert_configs = (
212+
cert_config.get("cert_configs") if isinstance(cert_config, dict) else None
213+
)
214+
workload_config = (
215+
cert_configs.get("workload") if isinstance(cert_configs, dict) else None
216+
)
217+
218+
if not isinstance(workload_config, dict) or "cert_path" not in workload_config:
219+
return None
220+
221+
return workload_config["cert_path"]
222+
223+
196224
def get_and_parse_agent_identity_certificate():
197225
"""Gets and parses the agent identity certificate if not opted out.
198226

0 commit comments

Comments
 (0)