gcpdiag.queries.apis

Build and cache GCP APIs + handle authentication.
AUTH_SCOPES = ['openid', 'https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/userinfo.email']
def set_credentials(cred_json):
82def set_credentials(cred_json):
83  global _credentials
84  if not cred_json:
85    _credentials = None
86  else:
87    _credentials = oauth2_credentials.Credentials.from_authorized_user_info(json.loads(cred_json))
def get_credentials():
 90def get_credentials():
 91  if _auth_method() == 'adc':
 92    return _get_credentials_adc()
 93  elif _auth_method() == 'key':
 94    return _get_credentials_key()
 95  else:
 96    raise AssertionError(
 97      'BUG: AUTH_METHOD method should be one of `adc` or `key`, '
 98      f'but got `{_auth_method()}` instead.'
 99      ' Please report at https://gcpdiag.dev/issues/'
100    )
def login():
110def login():
111  """Force GCP login (this otherwise happens on the first get_api call)."""
112  get_credentials()

Force GCP login (this otherwise happens on the first get_api call).

def get_user_email() -> str:
115def get_user_email() -> str:
116  if config.get('universe_domain') != 'googleapis.com':
117    return 'TPC user'
118  credentials = get_credentials().with_quota_project(None)
119
120  http = google_auth_httplib2.AuthorizedHttp(credentials, http=httplib2.Http())
121  resp, content = http.request('https://www.googleapis.com/userinfo/v2/me')
122  if resp['status'] != '200':
123    raise RuntimeError(f"can't determine user email. status={resp['status']}")
124  data = json.loads(content)
125  logging.debug('determined my email address: %s', data['email'])
126  return data['email']
@caching.cached_api_call(in_memory=True)
def get_api( service_name: str, version: str, project_id: Optional[str] = None, region: Optional[str] = None):
129@caching.cached_api_call(in_memory=True)
130def get_api(
131  service_name: str, version: str, project_id: Optional[str] = None, region: Optional[str] = None
132):
133  """Get an API object, as returned by googleapiclient.discovery.build.
134
135  If project_id is specified, this will be used as the billed project, and usually
136  you should put there the project id of the project that you are inspecting."""
137  credentials = get_credentials()
138
139  def _request_builder(http, *args, **kwargs):
140    del http
141
142    if 'headers' in kwargs:
143      # thread safety: make sure that original dictionary isn't modified
144      kwargs['headers'] = kwargs['headers'].copy()
145
146      headers = kwargs.get('headers', {})
147      headers['user-agent'] = f'gcpdiag/{config.VERSION} (gzip)'
148      if project_id:
149        headers['x-goog-user-project'] = _get_project_or_billing_id(project_id)
150
151    hooks.request_builder_hook(*args, **kwargs)
152
153    # thread safety: create a new AuthorizedHttp object for every request
154    # https://github.com/googleapis/google-api-python-client/blob/master/docs/thread_safety.md
155    new_http = google_auth_httplib2.AuthorizedHttp(credentials, http=httplib2.Http())
156    return googleapiclient.http.HttpRequest(new_http, *args, **kwargs)
157
158  universe_domain = config.get('universe_domain')
159  cred_universe = getattr(credentials, 'universe_domain', 'googleapis.com')
160  if cred_universe != universe_domain:
161    raise ValueError(f'credential universe_domain mismatch {cred_universe} != {universe_domain}')
162  client_options = ClientOptions()
163  if universe_domain != 'googleapis.com':
164    client_options.universe_domain = universe_domain
165  if region:
166    client_options.api_endpoint = f'https://{region}-{service_name}.{universe_domain}'
167  else:
168    client_options.api_endpoint = f'https://{service_name}.{universe_domain}'
169  if service_name in ['compute', 'bigquery', 'storage', 'dns']:
170    client_options.api_endpoint += f'/{service_name}/{version}'
171  api = discovery.build(
172    service_name,
173    version,
174    cache_discovery=False,
175    credentials=credentials,
176    requestBuilder=_request_builder,
177    client_options=client_options,
178  )
179  return api

Get an API object, as returned by googleapiclient.discovery.build.

If project_id is specified, this will be used as the billed project, and usually you should put there the project id of the project that you are inspecting.

def is_enabled(project_id: str, service_name: str) -> bool:
208def is_enabled(project_id: str, service_name: str) -> bool:
209  universe_domain = config.get('universe_domain')
210  return f'{service_name}.{universe_domain}' in _list_enabled_apis(project_id)
def is_all_enabled(project_id: str, services: list) -> Dict[str, str]:
213def is_all_enabled(project_id: str, services: list) -> Dict[str, str]:
214  service_state = {}
215  for service in services:
216    if not is_enabled(project_id, service):
217      service_state[service] = 'DISABLED'
218    else:
219      service_state[service] = 'ENABLED'
220  return service_state
@caching.cached_api_call(in_memory=True)
def list_services_with_state(project_id: str) -> Dict[str, str]:
223@caching.cached_api_call(in_memory=True)
224def list_services_with_state(project_id: str) -> Dict[str, str]:
225  logging.debug('listing all APIs with their state')
226  serviceusage = get_api('serviceusage', 'v1', project_id)
227  request = serviceusage.services().list(parent=f'projects/{project_id}')
228  apis_state: Dict[str, str] = {}
229  try:
230    while request is not None:
231      response = request.execute(num_retries=config.API_RETRIES)
232      for service in response['services']:
233        apis_state.setdefault(service['config']['name'], service['state'])
234      request = serviceusage.services().list_next(request, response)
235  except googleapiclient.errors.HttpError as err:
236    raise utils.GcpApiError(err) from err
237  return apis_state
def verify_access(project_id: str):
240def verify_access(project_id: str):
241  """Verify that the user has access to the project, exit with an error otherwise."""
242
243  try:
244    if not is_enabled(project_id, 'cloudresourcemanager'):
245      service = f'cloudresourcemanager.{config.get("universe_domain")}'
246      error_msg = (
247        'Cloud Resource Manager API must be enabled. To enable, execute:\n'
248        f'gcloud services enable {service} --project={project_id}'
249      )
250      raise utils.GcpApiError(response=error_msg, service=service, reason='SERVICE_DISABLED')
251    if not is_enabled(project_id, 'iam'):
252      service = f'iam.{config.get("universe_domain")}'
253      error_msg = (
254        'Identity and Access Management (IAM) API must be enabled. To enable, execute:\n'
255        f'gcloud services enable iam.{config.get("universe_domain")} --project={project_id}'
256      )
257      raise utils.GcpApiError(response=error_msg, service=service, reason='SERVICE_DISABLED')
258    if not is_enabled(project_id, 'logging'):
259      logging.warning(
260        'Cloud Logging API is not enabled (related rules will be skipped).'
261        ' To enable, execute:\ngcloud services enable'
262        ' logging.%s --project=%s',
263        config.get('universe_domain'),
264        project_id,
265      )
266  except utils.GcpApiError as err:
267    if 'SERVICE_DISABLED' == err.reason:
268      if f'serviceusage.{config.get("universe_domain")}' == err.service:
269        err.response += (
270          '\nService Usage API must be enabled. To enable, execute:\n'
271          f'gcloud services enable serviceusage.{config.get("universe_domain")} '
272          f'--project={project_id}'
273        )
274    else:
275      logging.error("can't access project %s: %s", project_id, err.message)
276    raise err
277  except exceptions.GoogleAuthError as err:
278    logging.error(err)
279    if _auth_method() == 'adc':
280      logging.error(
281        'Error using application default credentials. Try running: gcloud auth login --update-adc'
282      )
283    raise err
284  # Plug-in additional authorization verifications
285  hooks.verify_access_hook(project_id)

Verify that the user has access to the project, exit with an error otherwise.