File: //snap/google-cloud-cli/394/lib/googlecloudsdk/command_lib/apigee/defaults.py
# -*- coding: utf-8 -*- # Lint as: python3
# Copyright 2020 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Default values and fallbacks for missing surface arguments."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
import os
from googlecloudsdk.api_lib import apigee
from googlecloudsdk.calliope.concepts import deps
from googlecloudsdk.command_lib.apigee import errors
from googlecloudsdk.core import config
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import yaml
from googlecloudsdk.core.util import files
def _CachedDataWithName(name):
"""Returns the contents of a named cache file.
Cache files are saved as hidden YAML files in the gcloud config directory.
Args:
name: The name of the cache file.
Returns:
The decoded contents of the file, or an empty dictionary if the file could
not be read for whatever reason.
"""
config_dir = config.Paths().global_config_dir
cache_path = os.path.join(config_dir, ".apigee-cached-" + name)
if not os.path.isfile(cache_path):
return {}
try:
return yaml.load_path(cache_path)
except yaml.YAMLParseError:
# Another gcloud command might be in the process of writing to the file.
# Handle as a cache miss.
return {}
def _SaveCachedDataWithName(data, name):
"""Saves `data` to a named cache file.
Cache files are saved as hidden YAML files in the gcloud config directory.
Args:
data: The data to cache.
name: The name of the cache file.
"""
config_dir = config.Paths().global_config_dir
cache_path = os.path.join(config_dir, ".apigee-cached-" + name)
files.WriteFileContents(cache_path, yaml.dump(data))
def _DeleteCachedDataWithName(name):
"""Deletes a named cache file."""
config_dir = config.Paths().global_config_dir
cache_path = os.path.join(config_dir, ".apigee-cached-" + name)
if os.path.isfile(cache_path):
try:
os.remove(cache_path)
except OSError:
return
class Fallthrough(deps.Fallthrough):
"""Base class for Apigee resource argument fallthroughs."""
_handled_fields = []
def __init__(self, hint, active=False, plural=False):
super(Fallthrough, self).__init__(None, hint, active, plural)
def __contains__(self, field):
"""Returns whether `field` is handled by this fallthrough class."""
return field in self._handled_fields
def _Call(self, parsed_args):
raise NotImplementedError(
"Subclasses of googlecloudsdk.commnand_lib.apigee.Fallthrough must "
"actually provide a fallthrough."
)
def _GetProjectMapping(project, user_provided_org=None):
"""Returns the project mapping for the given GCP project.
Args:
project: The GCP project name.
user_provided_org: The organization ID provided by the user, if any.
Returns:
The project mapping for the given GCP project.
"""
project_mappings = _CachedDataWithName("project-mapping-v2") or {}
if user_provided_org:
mapping = project_mappings.get(user_provided_org, None)
if mapping:
return mapping
else:
try:
project_mapping = apigee.OrganizationsClient.ProjectMapping(
{"organizationsId": user_provided_org}
)
if "organization" not in project_mapping:
raise errors.UnauthorizedRequestError(
message=(
'Permission denied on resource "organizations/%s" (or it may'
" not exist)"
)
% user_provided_org
)
project_mappings[project] = project_mapping
_SaveCachedDataWithName(project_mappings, "project-mapping-v2")
return project_mapping
except (errors.EntityNotFoundError, errors.UnauthorizedRequestError):
raise errors.UnauthorizedRequestError(
message=(
'Permission denied on resource "organizations/%s" (or it may'
" not exist)"
)
% user_provided_org
)
except errors.RequestError as e:
raise e
if project not in project_mappings:
try:
project_mapping = apigee.OrganizationsClient.ProjectMapping(
{"organizationsId": project}
)
if "organization" not in project_mapping:
return None
if project_mapping.get("projectId", None) != project:
return None
project_mappings[project] = project_mapping
_SaveCachedDataWithName(project_mappings, "project-mapping-v2")
except (errors.EntityNotFoundError, errors.UnauthorizedRequestError):
return None
except errors.RequestError as e:
raise e
return project_mappings[project]
def _FindMappingForProject(project):
"""Returns the Apigee organization for the given GCP project."""
project_mapping = _CachedDataWithName("project-mapping-v2") or {}
if project in project_mapping:
return project_mapping[project]
# Listing organizations is an expensive operation for users with a lot of GCP
# projects. Since the GCP project -> Apigee organization mapping is immutable
# once created, cache known mappings to avoid the extra API call.
overrides = properties.VALUES.api_endpoint_overrides.apigee.Get()
if overrides:
list_orgs = apigee.OrganizationsClient.List()
else:
list_orgs = apigee.OrganizationsClient.ListOrganizationsGlobal()
for organization in list_orgs["organizations"]:
for matching_project in organization["projectIds"]:
project_mapping[matching_project] = {}
project_mapping[matching_project] = organization
_SaveCachedDataWithName(project_mapping, "project-mapping-v2")
_DeleteCachedDataWithName("project-mapping")
if project not in project_mapping:
return None
return project_mapping[project]
def OrganizationFromGCPProject():
"""Returns the organization associated with the active GCP project."""
project = properties.VALUES.core.project.Get()
if project is None:
log.warning("Neither Apigee organization nor GCP project is known.")
return None
# Use the cached project_mapping_v2 if available. This should handle all the
# cases where the project name is same as the organization name when cache
# miss happens.
project_mapping = _GetProjectMapping(project)
if project_mapping:
return project_mapping["organization"]
# Otherwise, list all organizations and update the project_mapping cache for
# all the projects in the response.
mapping = _FindMappingForProject(project)
if mapping:
return mapping["organization"]
log.warning("No Apigee organization is known for GCP project %s.", project)
log.warning(
"Please provide the argument [--organization] on the command "
"line, or set the property [api_endpoint_overrides/apigee]."
)
return None
class GCPProductOrganizationFallthrough(Fallthrough):
"""Falls through to the organization for the active GCP project."""
_handled_fields = ["organization"]
def __init__(self):
super(GCPProductOrganizationFallthrough, self).__init__(
"set the property [project] or provide the argument [--project] on the "
"command line, using a Cloud Platform project with an associated "
"Apigee organization"
)
def _Call(self, parsed_args):
return OrganizationFromGCPProject()
class StaticFallthrough(Fallthrough):
"""Falls through to a hardcoded value."""
def __init__(self, argument, value):
super(StaticFallthrough, self).__init__(
"leave the argument unspecified for it to be chosen automatically")
self._handled_fields = [argument]
self.value = value
def _Call(self, parsed_args):
return self.value
def FallBackToDeployedProxyRevision(args):
"""If `args` provides no revision, adds the deployed revision, if unambiguous.
Args:
args: a dictionary of resource identifiers which identifies an API proxy and
an environment, to which the deployed revision should be added.
Raises:
EntityNotFoundError: no deployment that matches `args` exists.
AmbiguousRequestError: more than one deployment matches `args`.
"""
deployments = apigee.DeploymentsClient.List(args)
if not deployments:
error_identifier = collections.OrderedDict([
("organization", args["organizationsId"]),
("environment", args["environmentsId"]), ("api", args["apisId"])
])
raise errors.EntityNotFoundError("deployment", error_identifier, "undeploy")
if len(deployments) > 1:
message = "Found more than one deployment that matches this request.\n"
raise errors.AmbiguousRequestError(message + yaml.dump(deployments))
deployed_revision = deployments[0]["revision"]
log.status.Print("Using deployed revision `%s`" % deployed_revision)
args["revisionsId"] = deployed_revision
def GetOrganizationLocation(organization):
"""Returns the location of the Apigee organization."""
project = properties.VALUES.core.project.Get()
mapping = _GetProjectMapping(project, organization)
if mapping:
return mapping.get("location", None)
# Project mapping is not available, assume projectId is not same as
# organization.
mapping = _FindMappingForProject(project)
if mapping:
return mapping.get("location", None)
log.warning("No Apigee organization is known for GCP project %s.", project)
log.warning(
"Please provide the argument [--organization] on the command "
"line, or set the property [api_endpoint_overrides/apigee]."
)
raise errors.LocationResolutionError()