1*c2e18aaaSAndroid Build Coastguard Worker#!/usr/bin/env python3 2*c2e18aaaSAndroid Build Coastguard Worker# Copyright 2024, The Android Open Source Project 3*c2e18aaaSAndroid Build Coastguard Worker# 4*c2e18aaaSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 5*c2e18aaaSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 6*c2e18aaaSAndroid Build Coastguard Worker# You may obtain a copy of the License at 7*c2e18aaaSAndroid Build Coastguard Worker# 8*c2e18aaaSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 9*c2e18aaaSAndroid Build Coastguard Worker# 10*c2e18aaaSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 11*c2e18aaaSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 12*c2e18aaaSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13*c2e18aaaSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 14*c2e18aaaSAndroid Build Coastguard Worker# limitations under the License. 15*c2e18aaaSAndroid Build Coastguard Worker 16*c2e18aaaSAndroid Build Coastguard Worker"""Rollout control for Atest features.""" 17*c2e18aaaSAndroid Build Coastguard Worker 18*c2e18aaaSAndroid Build Coastguard Workerimport functools 19*c2e18aaaSAndroid Build Coastguard Workerimport getpass 20*c2e18aaaSAndroid Build Coastguard Workerimport hashlib 21*c2e18aaaSAndroid Build Coastguard Workerimport importlib.resources 22*c2e18aaaSAndroid Build Coastguard Workerimport logging 23*c2e18aaaSAndroid Build Coastguard Workerimport os 24*c2e18aaaSAndroid Build Coastguard Workerfrom atest import atest_enum 25*c2e18aaaSAndroid Build Coastguard Workerfrom atest import atest_utils 26*c2e18aaaSAndroid Build Coastguard Workerfrom atest.metrics import metrics 27*c2e18aaaSAndroid Build Coastguard Worker 28*c2e18aaaSAndroid Build Coastguard Worker 29*c2e18aaaSAndroid Build Coastguard Worker@functools.cache 30*c2e18aaaSAndroid Build Coastguard Workerdef _get_project_owners() -> list[str]: 31*c2e18aaaSAndroid Build Coastguard Worker """Returns the owners of the feature.""" 32*c2e18aaaSAndroid Build Coastguard Worker owners = [] 33*c2e18aaaSAndroid Build Coastguard Worker try: 34*c2e18aaaSAndroid Build Coastguard Worker with importlib.resources.as_file( 35*c2e18aaaSAndroid Build Coastguard Worker importlib.resources.files('atest').joinpath('OWNERS') 36*c2e18aaaSAndroid Build Coastguard Worker ) as version_file_path: 37*c2e18aaaSAndroid Build Coastguard Worker owners.extend(version_file_path.read_text(encoding='utf-8').splitlines()) 38*c2e18aaaSAndroid Build Coastguard Worker except (ModuleNotFoundError, FileNotFoundError) as e: 39*c2e18aaaSAndroid Build Coastguard Worker logging.error(e) 40*c2e18aaaSAndroid Build Coastguard Worker try: 41*c2e18aaaSAndroid Build Coastguard Worker with importlib.resources.as_file( 42*c2e18aaaSAndroid Build Coastguard Worker importlib.resources.files('atest').joinpath('OWNERS_ADTE_TEAM') 43*c2e18aaaSAndroid Build Coastguard Worker ) as version_file_path: 44*c2e18aaaSAndroid Build Coastguard Worker owners.extend(version_file_path.read_text(encoding='utf-8').splitlines()) 45*c2e18aaaSAndroid Build Coastguard Worker except (ModuleNotFoundError, FileNotFoundError) as e: 46*c2e18aaaSAndroid Build Coastguard Worker logging.error(e) 47*c2e18aaaSAndroid Build Coastguard Worker return [line.split('@')[0] for line in owners if '@google.com' in line] 48*c2e18aaaSAndroid Build Coastguard Worker 49*c2e18aaaSAndroid Build Coastguard Worker 50*c2e18aaaSAndroid Build Coastguard Workerclass RolloutControlledFeature: 51*c2e18aaaSAndroid Build Coastguard Worker """Base class for Atest features under rollout control.""" 52*c2e18aaaSAndroid Build Coastguard Worker 53*c2e18aaaSAndroid Build Coastguard Worker def __init__( 54*c2e18aaaSAndroid Build Coastguard Worker self, 55*c2e18aaaSAndroid Build Coastguard Worker name: str, 56*c2e18aaaSAndroid Build Coastguard Worker rollout_percentage: float, 57*c2e18aaaSAndroid Build Coastguard Worker env_control_flag: str, 58*c2e18aaaSAndroid Build Coastguard Worker feature_id: int = None, 59*c2e18aaaSAndroid Build Coastguard Worker owners: list[str] | None = None, 60*c2e18aaaSAndroid Build Coastguard Worker print_message: str | None = None, 61*c2e18aaaSAndroid Build Coastguard Worker ): 62*c2e18aaaSAndroid Build Coastguard Worker """Initializes the object. 63*c2e18aaaSAndroid Build Coastguard Worker 64*c2e18aaaSAndroid Build Coastguard Worker Args: 65*c2e18aaaSAndroid Build Coastguard Worker name: The name of the feature. 66*c2e18aaaSAndroid Build Coastguard Worker rollout_percentage: The percentage of users to enable the feature for. 67*c2e18aaaSAndroid Build Coastguard Worker The value should be in [0, 100]. 68*c2e18aaaSAndroid Build Coastguard Worker env_control_flag: The environment variable name to override the feature 69*c2e18aaaSAndroid Build Coastguard Worker enablement. When set, 'true' or '1' means enable, other values means 70*c2e18aaaSAndroid Build Coastguard Worker disable. 71*c2e18aaaSAndroid Build Coastguard Worker feature_id: The ID of the feature that is controlled by rollout control 72*c2e18aaaSAndroid Build Coastguard Worker for metric collection purpose. Must be a positive integer. 73*c2e18aaaSAndroid Build Coastguard Worker owners: The owners of the feature. If not provided, the owners of the 74*c2e18aaaSAndroid Build Coastguard Worker feature will be read from OWNERS file. 75*c2e18aaaSAndroid Build Coastguard Worker print_message: The message to print to the console when the feature is 76*c2e18aaaSAndroid Build Coastguard Worker enabled for the user. 77*c2e18aaaSAndroid Build Coastguard Worker """ 78*c2e18aaaSAndroid Build Coastguard Worker if rollout_percentage < 0 or rollout_percentage > 100: 79*c2e18aaaSAndroid Build Coastguard Worker raise ValueError( 80*c2e18aaaSAndroid Build Coastguard Worker 'Rollout percentage must be in [0, 100]. Got %s instead.' 81*c2e18aaaSAndroid Build Coastguard Worker % rollout_percentage 82*c2e18aaaSAndroid Build Coastguard Worker ) 83*c2e18aaaSAndroid Build Coastguard Worker if feature_id is not None and feature_id <= 0: 84*c2e18aaaSAndroid Build Coastguard Worker raise ValueError( 85*c2e18aaaSAndroid Build Coastguard Worker 'Feature ID must be a positive integer. Got %s instead.' % feature_id 86*c2e18aaaSAndroid Build Coastguard Worker ) 87*c2e18aaaSAndroid Build Coastguard Worker if owners is None: 88*c2e18aaaSAndroid Build Coastguard Worker owners = _get_project_owners() 89*c2e18aaaSAndroid Build Coastguard Worker self._name = name 90*c2e18aaaSAndroid Build Coastguard Worker self._rollout_percentage = rollout_percentage 91*c2e18aaaSAndroid Build Coastguard Worker self._env_control_flag = env_control_flag 92*c2e18aaaSAndroid Build Coastguard Worker self._feature_id = feature_id 93*c2e18aaaSAndroid Build Coastguard Worker self._owners = owners 94*c2e18aaaSAndroid Build Coastguard Worker self._print_message = print_message 95*c2e18aaaSAndroid Build Coastguard Worker 96*c2e18aaaSAndroid Build Coastguard Worker def _check_env_control_flag(self) -> bool | None: 97*c2e18aaaSAndroid Build Coastguard Worker """Checks the environment variable to override the feature enablement. 98*c2e18aaaSAndroid Build Coastguard Worker 99*c2e18aaaSAndroid Build Coastguard Worker Returns: 100*c2e18aaaSAndroid Build Coastguard Worker True if the feature is enabled, False if disabled, None if not set. 101*c2e18aaaSAndroid Build Coastguard Worker """ 102*c2e18aaaSAndroid Build Coastguard Worker if self._env_control_flag not in os.environ: 103*c2e18aaaSAndroid Build Coastguard Worker return None 104*c2e18aaaSAndroid Build Coastguard Worker return os.environ[self._env_control_flag] in ('TRUE', 'True', 'true', '1') 105*c2e18aaaSAndroid Build Coastguard Worker 106*c2e18aaaSAndroid Build Coastguard Worker def _is_enabled_for_user(self, username: str | None) -> bool: 107*c2e18aaaSAndroid Build Coastguard Worker """Checks whether the feature is enabled for the user. 108*c2e18aaaSAndroid Build Coastguard Worker 109*c2e18aaaSAndroid Build Coastguard Worker Args: 110*c2e18aaaSAndroid Build Coastguard Worker username: The username to check the feature enablement for. If not 111*c2e18aaaSAndroid Build Coastguard Worker provided, the current user's username will be used. 112*c2e18aaaSAndroid Build Coastguard Worker 113*c2e18aaaSAndroid Build Coastguard Worker Returns: 114*c2e18aaaSAndroid Build Coastguard Worker True if the feature is enabled for the user, False otherwise. 115*c2e18aaaSAndroid Build Coastguard Worker """ 116*c2e18aaaSAndroid Build Coastguard Worker if self._rollout_percentage == 100: 117*c2e18aaaSAndroid Build Coastguard Worker return True 118*c2e18aaaSAndroid Build Coastguard Worker 119*c2e18aaaSAndroid Build Coastguard Worker if username is None: 120*c2e18aaaSAndroid Build Coastguard Worker username = getpass.getuser() 121*c2e18aaaSAndroid Build Coastguard Worker 122*c2e18aaaSAndroid Build Coastguard Worker if not username: 123*c2e18aaaSAndroid Build Coastguard Worker logging.debug( 124*c2e18aaaSAndroid Build Coastguard Worker 'Unable to determine the username. Disabling the feature %s.', 125*c2e18aaaSAndroid Build Coastguard Worker self._name, 126*c2e18aaaSAndroid Build Coastguard Worker ) 127*c2e18aaaSAndroid Build Coastguard Worker return False 128*c2e18aaaSAndroid Build Coastguard Worker 129*c2e18aaaSAndroid Build Coastguard Worker if username in self._owners: 130*c2e18aaaSAndroid Build Coastguard Worker return True 131*c2e18aaaSAndroid Build Coastguard Worker 132*c2e18aaaSAndroid Build Coastguard Worker hash_object = hashlib.sha256() 133*c2e18aaaSAndroid Build Coastguard Worker hash_object.update((username + ' ' + self._name).encode('utf-8')) 134*c2e18aaaSAndroid Build Coastguard Worker return int(hash_object.hexdigest(), 16) % 100 < self._rollout_percentage 135*c2e18aaaSAndroid Build Coastguard Worker 136*c2e18aaaSAndroid Build Coastguard Worker @functools.cache 137*c2e18aaaSAndroid Build Coastguard Worker def is_enabled(self, username: str | None = None) -> bool: 138*c2e18aaaSAndroid Build Coastguard Worker """Checks whether the current feature is enabled for the user. 139*c2e18aaaSAndroid Build Coastguard Worker 140*c2e18aaaSAndroid Build Coastguard Worker Args: 141*c2e18aaaSAndroid Build Coastguard Worker username: The username to check the feature enablement for. If not 142*c2e18aaaSAndroid Build Coastguard Worker provided, the current user's username will be used. 143*c2e18aaaSAndroid Build Coastguard Worker 144*c2e18aaaSAndroid Build Coastguard Worker Returns: 145*c2e18aaaSAndroid Build Coastguard Worker True if the feature is enabled for the user, False otherwise. 146*c2e18aaaSAndroid Build Coastguard Worker """ 147*c2e18aaaSAndroid Build Coastguard Worker override_flag_value = self._check_env_control_flag() 148*c2e18aaaSAndroid Build Coastguard Worker if override_flag_value is not None: 149*c2e18aaaSAndroid Build Coastguard Worker logging.debug( 150*c2e18aaaSAndroid Build Coastguard Worker 'Feature %s is %s by env variable %s.', 151*c2e18aaaSAndroid Build Coastguard Worker self._name, 152*c2e18aaaSAndroid Build Coastguard Worker 'enabled' if override_flag_value else 'disabled', 153*c2e18aaaSAndroid Build Coastguard Worker self._env_control_flag, 154*c2e18aaaSAndroid Build Coastguard Worker ) 155*c2e18aaaSAndroid Build Coastguard Worker if self._feature_id: 156*c2e18aaaSAndroid Build Coastguard Worker metrics.LocalDetectEvent( 157*c2e18aaaSAndroid Build Coastguard Worker detect_type=atest_enum.DetectType.ROLLOUT_CONTROLLED_FEATURE_ID_OVERRIDE, 158*c2e18aaaSAndroid Build Coastguard Worker result=self._feature_id 159*c2e18aaaSAndroid Build Coastguard Worker if override_flag_value 160*c2e18aaaSAndroid Build Coastguard Worker else -self._feature_id, 161*c2e18aaaSAndroid Build Coastguard Worker ) 162*c2e18aaaSAndroid Build Coastguard Worker return override_flag_value 163*c2e18aaaSAndroid Build Coastguard Worker 164*c2e18aaaSAndroid Build Coastguard Worker is_enabled = self._is_enabled_for_user(username) 165*c2e18aaaSAndroid Build Coastguard Worker 166*c2e18aaaSAndroid Build Coastguard Worker logging.debug( 167*c2e18aaaSAndroid Build Coastguard Worker 'Feature %s is %s for user %s.', 168*c2e18aaaSAndroid Build Coastguard Worker self._name, 169*c2e18aaaSAndroid Build Coastguard Worker 'enabled' if is_enabled else 'disabled', 170*c2e18aaaSAndroid Build Coastguard Worker username, 171*c2e18aaaSAndroid Build Coastguard Worker ) 172*c2e18aaaSAndroid Build Coastguard Worker 173*c2e18aaaSAndroid Build Coastguard Worker if self._feature_id: 174*c2e18aaaSAndroid Build Coastguard Worker metrics.LocalDetectEvent( 175*c2e18aaaSAndroid Build Coastguard Worker detect_type=atest_enum.DetectType.ROLLOUT_CONTROLLED_FEATURE_ID, 176*c2e18aaaSAndroid Build Coastguard Worker result=self._feature_id if is_enabled else -self._feature_id, 177*c2e18aaaSAndroid Build Coastguard Worker ) 178*c2e18aaaSAndroid Build Coastguard Worker 179*c2e18aaaSAndroid Build Coastguard Worker if is_enabled and self._print_message: 180*c2e18aaaSAndroid Build Coastguard Worker print(atest_utils.mark_magenta(self._print_message)) 181*c2e18aaaSAndroid Build Coastguard Worker 182*c2e18aaaSAndroid Build Coastguard Worker return is_enabled 183*c2e18aaaSAndroid Build Coastguard Worker 184*c2e18aaaSAndroid Build Coastguard Worker 185*c2e18aaaSAndroid Build Coastguard Workerdeprecate_bazel_mode = RolloutControlledFeature( 186*c2e18aaaSAndroid Build Coastguard Worker name='Deprecate Bazel Mode', 187*c2e18aaaSAndroid Build Coastguard Worker rollout_percentage=60, 188*c2e18aaaSAndroid Build Coastguard Worker env_control_flag='DEPRECATE_BAZEL_MODE', 189*c2e18aaaSAndroid Build Coastguard Worker feature_id=1, 190*c2e18aaaSAndroid Build Coastguard Worker) 191*c2e18aaaSAndroid Build Coastguard Worker 192*c2e18aaaSAndroid Build Coastguard Workerrolling_tf_subprocess_output = RolloutControlledFeature( 193*c2e18aaaSAndroid Build Coastguard Worker name='Rolling TradeFed subprocess output', 194*c2e18aaaSAndroid Build Coastguard Worker rollout_percentage=100, 195*c2e18aaaSAndroid Build Coastguard Worker env_control_flag='ROLLING_TF_SUBPROCESS_OUTPUT', 196*c2e18aaaSAndroid Build Coastguard Worker feature_id=2, 197*c2e18aaaSAndroid Build Coastguard Worker print_message=( 198*c2e18aaaSAndroid Build Coastguard Worker 'You are one of the first users receiving the "Rolling subprocess' 199*c2e18aaaSAndroid Build Coastguard Worker ' output" feature. If you are happy with it, please +1 on' 200*c2e18aaaSAndroid Build Coastguard Worker ' http://b/380460196.' 201*c2e18aaaSAndroid Build Coastguard Worker ), 202*c2e18aaaSAndroid Build Coastguard Worker) 203*c2e18aaaSAndroid Build Coastguard Worker 204*c2e18aaaSAndroid Build Coastguard Workertf_preparer_incremental_setup = RolloutControlledFeature( 205*c2e18aaaSAndroid Build Coastguard Worker name='TradeFed preparer incremental setup', 206*c2e18aaaSAndroid Build Coastguard Worker rollout_percentage=0, 207*c2e18aaaSAndroid Build Coastguard Worker env_control_flag='TF_PREPARER_INCREMENTAL_SETUP', 208*c2e18aaaSAndroid Build Coastguard Worker feature_id=3, 209*c2e18aaaSAndroid Build Coastguard Worker print_message=( 210*c2e18aaaSAndroid Build Coastguard Worker 'You are one of the first users selected to receive the "Incremental' 211*c2e18aaaSAndroid Build Coastguard Worker ' setup for TradeFed preparers" feature. If you are happy with it,' 212*c2e18aaaSAndroid Build Coastguard Worker ' please +1 on http://b/381900378. If you experienced any issues,' 213*c2e18aaaSAndroid Build Coastguard Worker ' please comment on the same bug.' 214*c2e18aaaSAndroid Build Coastguard Worker ), 215*c2e18aaaSAndroid Build Coastguard Worker) 216