Skip to content

Commit 06cad19

Browse files
xuming-msCopilot
andcommitted
CI: an Azure Pipelines run of the managed-environment gate
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1a664db commit 06cad19

2 files changed

Lines changed: 162 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
#
6+
# Throwaway pipeline. Shows on a real Azure Pipelines agent that the plaintext fallback warning is
7+
# suppressed there, which the unit tests cannot show because they mock the environment.
8+
#
9+
# Set it up by hand in azclitools/internal, pointing at this file on this branch, then run it and
10+
# delete both. Nothing here needs a service connection or a sign-in.
11+
12+
name: encryption-fallback-warning-$(Date:yyyyMMdd)$(Rev:.r)
13+
14+
trigger: none
15+
pr: none
16+
17+
pool:
18+
vmImage: ubuntu-latest
19+
20+
steps:
21+
- task: UsePythonVersion@0
22+
inputs:
23+
versionSpec: '3.12'
24+
25+
- script: |
26+
set -ev
27+
python -m pip install --upgrade pip
28+
pip install ./src/azure-cli-telemetry
29+
pip install ./src/azure-cli-core
30+
pip install pytest
31+
displayName: Install azure-cli-core from source
32+
33+
- script: |
34+
python -m pytest \
35+
src/azure-cli-core/azure/cli/core/auth/tests/test_persistence.py \
36+
"src/azure-cli-core/azure/cli/core/tests/test_util.py::TestUtils::test_in_managed_environment" -q
37+
displayName: Unit tests
38+
39+
- script: |
40+
echo "DBUS_SESSION_BUS_ADDRESS=${DBUS_SESSION_BUS_ADDRESS:-<unset>}"
41+
python -c "import gi" 2>&1 || echo "python3-gi is not importable"
42+
displayName: Report what the agent offers
43+
44+
- script: python .github/scripts/check_managed_environment.py
45+
displayName: The warning is suppressed, and only because this is an agent
46+
env:
47+
# An unreachable D-Bus makes libsecret fail regardless of what the agent image ships, so the
48+
# fallback is guaranteed to happen and there is always a warning to suppress.
49+
DBUS_SESSION_BUS_ADDRESS: unix:path=/nonexistent
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python
2+
# --------------------------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for license information.
5+
# --------------------------------------------------------------------------------------------
6+
7+
"""Assert the plaintext fallback warning is suppressed on a real platform-managed agent.
8+
9+
Meant to be run on an Azure Pipelines or GitHub Actions agent, and needs no sign-in: the warning is
10+
decided by build_persistence and warn_if_encryption_unavailable alone.
11+
12+
The unit tests mock the environment, so they cannot show that an agent really sets TF_BUILD or
13+
GITHUB_ACTIONS. This runs the same code against whatever the agent actually provides, then repeats
14+
it with those variables removed, so a pass means the gate is what silenced the warning and not a
15+
fallback that never happened.
16+
"""
17+
18+
import logging
19+
import os
20+
import sys
21+
import tempfile
22+
23+
from azure.cli.core.auth import persistence
24+
from azure.cli.core.util import in_ci, in_managed_environment
25+
26+
CI_VARIABLES = ('TF_BUILD', 'GITHUB_ACTIONS', 'CI')
27+
28+
29+
def _collect(level):
30+
"""Capture messages the persistence logger emits at one level."""
31+
messages = []
32+
handler = logging.Handler()
33+
handler.emit = lambda record: messages.append(record.getMessage())
34+
handler.setLevel(level)
35+
return messages, handler
36+
37+
38+
def _fall_back():
39+
"""Ask for encryption on a machine with no usable keyring, and return the debug log."""
40+
debug, handler = _collect(logging.DEBUG)
41+
persistence.logger.addHandler(handler)
42+
persistence.logger.setLevel(logging.DEBUG)
43+
try:
44+
persistence._encryption_fallback = False # pylint: disable=protected-access
45+
with tempfile.TemporaryDirectory() as directory:
46+
store = persistence.build_persistence(directory + '/probe', True, type='Token cache')
47+
finally:
48+
persistence.logger.removeHandler(handler)
49+
return store, debug
50+
51+
52+
def _warnings_from_sign_in():
53+
warnings, handler = _collect(logging.WARNING)
54+
persistence.logger.addHandler(handler)
55+
try:
56+
persistence.warn_if_encryption_unavailable()
57+
finally:
58+
persistence.logger.removeHandler(handler)
59+
return [message for message in warnings if message == persistence.ENCRYPTION_FALLBACK_WARNING]
60+
61+
62+
def main():
63+
seen = {name: os.environ.get(name) for name in CI_VARIABLES}
64+
print('agent environment: ' + ', '.join(
65+
f'{name}={value!r}' if value is not None else f'{name}=<unset>' for name, value in seen.items()))
66+
print(f'in_ci()={in_ci()}, in_managed_environment()={in_managed_environment()}')
67+
68+
failures = []
69+
if not in_ci():
70+
failures.append('this agent sets none of the variables in_ci() looks for, so the gate this '
71+
'checks would never apply here')
72+
if not in_managed_environment():
73+
failures.append('in_managed_environment() is False on a CI agent')
74+
75+
# The fallback itself has to happen, or there would be no warning to suppress and a pass would
76+
# mean nothing.
77+
store, debug = _fall_back()
78+
if store.is_encrypted:
79+
failures.append('the persistence reports itself as encrypted, so there was no fallback to '
80+
'observe on this agent')
81+
if not isinstance(store, persistence.FilePersistence):
82+
failures.append(f'expected a plaintext FilePersistence, got {type(store).__name__}')
83+
if not persistence._encryption_fallback: # pylint: disable=protected-access
84+
failures.append('the fallback was not recorded')
85+
if not any('Failed to initialize LibsecretPersistence' in message for message in debug):
86+
failures.append('the reason libsecret was unusable never reached the debug log')
87+
88+
if _warnings_from_sign_in():
89+
failures.append('sign-in warned about plaintext storage on a platform-managed agent, where '
90+
'the user cannot act on it')
91+
92+
# Same process, same fallback, with only the CI variables taken away. Without this a silent
93+
# warning path would pass just as happily as a working gate.
94+
removed = {name: os.environ.pop(name) for name in CI_VARIABLES if name in os.environ}
95+
try:
96+
if not _warnings_from_sign_in():
97+
failures.append(f'with {", ".join(removed)} removed the sign-in still said nothing, so '
98+
'the silence above was not the gate')
99+
finally:
100+
os.environ.update(removed)
101+
102+
for failure in failures:
103+
print(f'##vso[task.logissue type=error]{failure}')
104+
if failures:
105+
return 1
106+
107+
print('the fallback happened, was explained in the debug log, and the warning was suppressed '
108+
'only because this is a platform-managed agent')
109+
return 0
110+
111+
112+
if __name__ == '__main__':
113+
sys.exit(main())

0 commit comments

Comments
 (0)