feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,390 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""Functionality related to Cloud Run Integration API clients."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import re
from typing import List, Optional
from apitools.base.py import encoding as apitools_encoding
from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.api_lib.util import exceptions as api_lib_exceptions
from googlecloudsdk.api_lib.util import waiter
from googlecloudsdk.command_lib.runapps import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import resources
from googlecloudsdk.core.util import encoding
from googlecloudsdk.core.util import retry
from googlecloudsdk.generated_clients.apis.runapps.v1alpha1 import runapps_v1alpha1_client
from googlecloudsdk.generated_clients.apis.runapps.v1alpha1 import runapps_v1alpha1_messages
API_NAME = 'runapps'
API_VERSION = 'v1alpha1'
# Key for the config field of application dictionary.
APP_DICT_CONFIG_KEY = 'config'
# Key for the resource field within config field of application dictionary.
APP_CONFIG_DICT_RESOURCES_KEY = 'resources'
# Max wait time before timing out, match timeout of CP
_POLLING_TIMEOUT_MS = 30 * 60 * 1000
# Max wait time between poll retries before timing out
_RETRY_TIMEOUT_MS = 1000
_LOCATION_ERROR_REGEX = re.compile(r'Location [\w-]+ is not found')
def GetMessages():
"""Returns the messages module for the Runapps API.
Returns:
Module containing the definitions of messages for the Runapps API.
"""
return apis.GetMessagesModule(API_NAME, API_VERSION)
def GetApplication(
client: runapps_v1alpha1_client.RunappsV1alpha1,
app_ref: resources) -> Optional[runapps_v1alpha1_messages.Application]:
"""Calls GetApplication API of Runapps for the specified reference.
Args:
client: The api client to use.
app_ref: The resource reference of the application.
Raises:
exceptions.UnsupportedIntegrationsLocationError: if the region does not
exist for the user.
Returns:
The application. If the application does not exist, then
None is returned.
"""
request = (
client.MESSAGES_MODULE.RunappsProjectsLocationsApplicationsGetRequest(
name=app_ref.RelativeName())
)
try:
return client.projects_locations_applications.Get(request)
except apitools_exceptions.HttpNotFoundError:
return None
except apitools_exceptions.HttpForbiddenError as e:
_HandleLocationError(app_ref.locationsId, e)
def ListApplications(
client: runapps_v1alpha1_client.RunappsV1alpha1, app_ref: resources
) -> runapps_v1alpha1_messages.ListApplicationsResponse:
"""Calls ListApplications API of Runapps for the specified reference."""
request = (
client.MESSAGES_MODULE.RunappsProjectsLocationsApplicationsListRequest(
parent=app_ref.RelativeName()
)
)
response = client.projects_locations_applications.List(request)
if response.unreachable:
log.warning(
'The following regions did not respond: {}. '
'List results may be incomplete'.format(
', '.join(sorted(response.unreachable))
)
)
return response
def GetApplicationStatus(
client: runapps_v1alpha1_client.RunappsV1alpha1,
app_ref: resources,
resource_ids: Optional[List[runapps_v1alpha1_messages.ResourceID]] = None,
) -> Optional[runapps_v1alpha1_messages.ApplicationStatus]:
"""Calls GetApplicationStatus API of Runapps for the specified reference.
Args:
client: the api client to use.
app_ref: the resource reference of the application.
resource_ids: ResourceID of the resource to get status for. If not given,
all resources in the application will be queried.
Returns:
The ApplicationStatus object. Or None if not found.
"""
if resource_ids:
res_filters = [
res_id.type + '/' + res_id.name for res_id in resource_ids
]
else:
res_filters = []
module = client.MESSAGES_MODULE
request = module.RunappsProjectsLocationsApplicationsGetStatusRequest(
name=app_ref.RelativeName(), resources=res_filters
)
try:
return client.projects_locations_applications.GetStatus(request)
except apitools_exceptions.HttpNotFoundError:
return None
def CreateApplication(
client: runapps_v1alpha1_client.RunappsV1alpha1,
app_ref: resources,
application: runapps_v1alpha1_messages.Application
) -> runapps_v1alpha1_messages.Operation:
"""Calls CreateApplicaton API of Runapps for the specified reference.
Args:
client: the api client to use.
app_ref: the resource reference of
the application.
application: the application to create
Returns:
the LRO of this request.
"""
return client.projects_locations_applications.Create(
client.MESSAGES_MODULE.RunappsProjectsLocationsApplicationsCreateRequest(
application=application,
applicationId=application.name,
parent=app_ref.Parent().RelativeName()))
def PatchApplication(
client: runapps_v1alpha1_client.RunappsV1alpha1,
app_ref: resources,
application: runapps_v1alpha1_messages.Application,
update_mask: Optional[str] = None) -> runapps_v1alpha1_messages.Operation:
"""Calls ApplicationPatch API of Runapps for the specified reference.
Args:
client: the api client to use.
app_ref: the resource reference of
the application.
application: the application to patch
update_mask: comma separated string listing the fields to be updated.
Returns:
the LRO of this request.
"""
return client.projects_locations_applications.Patch(
client.MESSAGES_MODULE.RunappsProjectsLocationsApplicationsPatchRequest(
application=application,
updateMask=update_mask,
name=app_ref.RelativeName()))
def CreateDeployment(
client: runapps_v1alpha1_client.RunappsV1alpha1,
app_ref: resources,
deployment: runapps_v1alpha1_messages.Deployment,
validate_only: Optional[bool] = False
) -> runapps_v1alpha1_messages.Operation:
"""Calls CreateDeployment API of Runapps.
Args:
client: the api client to use.
app_ref: the resource reference of the application the deployment belongs to
deployment: the deployment object
validate_only: whether to only validate the deployment
Returns:
the LRO of this request.
"""
return client.projects_locations_applications_deployments.Create(
client.MESSAGES_MODULE
.RunappsProjectsLocationsApplicationsDeploymentsCreateRequest(
parent=app_ref.RelativeName(),
deployment=deployment,
deploymentId=deployment.name,
validateOnly=validate_only)
)
def GetDeployment(
client: runapps_v1alpha1_client.RunappsV1alpha1,
deployment_name: str) -> Optional[runapps_v1alpha1_messages.Deployment]:
"""Calls GetDeployment API of Runapps.
Args:
client: the api client to use.
deployment_name: the canonical name of the deployment. For example:
projects/<project>/locations/<location>/applications/<app>/deployment/<id>
Returns:
the Deployment object. None is returned if the deployment cannot be found.
"""
try:
return client.projects_locations_applications_deployments.Get(
client.MESSAGES_MODULE
.RunappsProjectsLocationsApplicationsDeploymentsGetRequest(
name=deployment_name)
)
except apitools_exceptions.HttpNotFoundError:
return None
def WaitForApplicationOperation(
client: runapps_v1alpha1_client.RunappsV1alpha1,
operation: runapps_v1alpha1_messages.Operation
) -> runapps_v1alpha1_messages.Application:
"""Waits for an operation to complete.
Args:
client: client used to make requests.
operation: object to wait for.
Returns:
the application from the operation.
"""
return _WaitForOperation(client, operation,
client.projects_locations_applications)
def WaitForDeploymentOperation(
client: runapps_v1alpha1_client.RunappsV1alpha1,
operation: runapps_v1alpha1_messages.Operation,
tracker, tracker_update_func) -> runapps_v1alpha1_messages.Deployment:
"""Waits for an operation to complete.
Args:
client: client used to make requests.
operation: object to wait for.
tracker: The ProgressTracker that tracks the operation progress.
tracker_update_func: function to update the tracker on polling.
Returns:
the deployment from thex operation.
"""
return _WaitForOperation(client, operation,
client.projects_locations_applications_deployments,
tracker, tracker_update_func)
def _WaitForOperation(client: runapps_v1alpha1_client.RunappsV1alpha1,
operation: runapps_v1alpha1_messages.Operation,
resource_type,
tracker=None,
tracker_update_func=None):
"""Waits for an operation to complete.
Args:
client: client used to make requests.
operation: object to wait for.
resource_type: type, the expected type of resource response
tracker: The ProgressTracker that tracks the operation progress.
tracker_update_func: function to update the tracker on polling.
Returns:
The resulting resource of input paramater resource_type.
"""
poller = waiter.CloudOperationPoller(resource_type,
client.projects_locations_operations)
operation_ref = resources.REGISTRY.ParseRelativeName(
operation.name,
collection='{}.projects.locations.operations'.format(API_NAME))
def _StatusUpdate(result, status):
if tracker is None:
return
if tracker_update_func:
tracker_update_func(tracker, result, status)
else:
tracker.Tick()
try:
return poller.GetResult(
waiter.PollUntilDone(
poller,
operation_ref,
max_wait_ms=_POLLING_TIMEOUT_MS,
wait_ceiling_ms=_RETRY_TIMEOUT_MS,
status_update=_StatusUpdate))
except waiter.OperationError:
operation = poller.Poll(operation_ref)
raise exceptions.IntegrationsOperationError(
'OperationError: code={0}, message={1}'.format(
operation.error.code, encoding.Decode(operation.error.message)))
except retry.WaitException:
# Operation timed out.
raise waiter.TimeoutError(
'Operation timed out after {0} seconds. The operations may still '
'be underway remotely and may still succeed.'
.format(_POLLING_TIMEOUT_MS / 1000))
def GetDeploymentOperationMetadata(
messages,
operation: runapps_v1alpha1_messages.Operation
) -> runapps_v1alpha1_messages.DeploymentOperationMetadata:
"""Get the metadata message for the deployment operation.
Args:
messages: Module containing the definitions of messages for the Runapps
API.
operation: The LRO
Returns:
The DeploymentOperationMetadata object.
"""
return apitools_encoding.PyValueToMessage(
messages.DeploymentOperationMetadata,
apitools_encoding.MessageToPyValue(operation.metadata))
def ListLocations(
client: runapps_v1alpha1_client.RunappsV1alpha1,
proj_id: str) -> runapps_v1alpha1_messages.ListLocationsResponse:
"""Get the list of all available regions from control plane.
Args:
client: instance of a client to use for the list request.
proj_id: project id of the project to query.
Returns:
A list of location resources.
"""
request = client.MESSAGES_MODULE.RunappsProjectsLocationsListRequest(
name='projects/{0}'.format(proj_id)
)
return client.projects_locations.List(request)
def _HandleLocationError(region: str, error: Exception) -> Exception:
"""Get the metadata message for the deployment operation.
Args:
region: target region of the request.
error: original HttpError.
Raises:
UnsupportedIntegrationsLocationError if it's location error. Otherwise
raise the original error.
"""
parsed_err = api_lib_exceptions.HttpException(error)
if _LOCATION_ERROR_REGEX.match(parsed_err.payload.status_message):
raise exceptions.UnsupportedIntegrationsLocationError(
'Location {} is not found or access is unauthorized.'.format(region)
)
raise error

View File

@@ -0,0 +1,262 @@
integrations:
- integration_type: custom-domains
resource_type: router
singleton_name: custom-domains
required_field: domains
visible: true
service_type: ingress
label: Custom Domains
product: Cloud Load Balancer
description: Configure custom domains for Cloud Run services with Google Cloud Load Balancer.
example_command: |-
Create the integration to add the first domain mapping:
$ gcloud {track} run integrations create --type=custom-domains --parameters='set-mapping=example.com/*:[SERVICE]'
Update the integration to add subsequent mappings:
$ gcloud {track} run integrations update custom-domains --parameters='set-mapping=anotherexample.com/*:[SERVICE]'
example_yaml: |
apiVersion: runapps.googleapis.com/v1alpha1
resources:
- id: router/custom-domains
subresources:
- id: domain/example-com
config:
domain: example.com
bindings:
- target: service/[SERVICE]
config:
paths: ["/*"]
parameters:
- name: set-mapping
description: 'Set a route mapping from a path to a service. Format: set-mapping=[DOMAIN]/[PATH]:[SERVICE]'
required: true
data_type: domain-path-service
- name: remove-mapping
description: 'Remove a route mapping. Format: remove-mapping=[DOMAIN]/[PATH]'
data_type: domain-path
create_allowed: false
- name: remove-domain
description: To remove a domain an all of its route mappings.
data_type: domain
create_allowed: false
update_exclusive_groups:
- params:
- set-mapping
- remove-mapping
- remove-domain
disable_service_flags: true
required_apis:
- compute.googleapis.com
- integration_type: redis
resource_type: redis
label: 'Redis'
product: Cloud Memorystore
description: Configure a Redis instance (Cloud Memorystore) and connect it to a Cloud Run Service.
example_command: |-
$ gcloud {track} run integrations create --service=[SERVICE] --type=redis --parameters=memory-size-gb=2
example_yaml: |
apiVersion: runapps.googleapis.com/v1alpha1
resources:
- id: redis/redis-1
config:
memorySizeGb: 2
- id: service/[SERVICE]
bindings:
- target: redis/redis-1
service_type: backing
visible: true
eta_in_min: 10
cta: >-
To connect to the Redis instance utilize the environment variables REDISHOST and REDISPORT.
These have been added to the Cloud Run service for you.
parameters:
- name: memory-size-gb
config_name: memorySizeGb
label: Capacity (GB)
description: Memory capacity of the Redis instance.
data_type: int
default: 1
- name: tier
label: Service Tier
description: >
The service tier of the instance. Supported options include BASIC for standalone
instance and STANDARD_HA for highly available primary/replica instances.
data_type: string
hidden: true
- name: version
label: Version
description: >
The version of Redis software. If not provided, latest supported version will be used.
Supported values include: REDIS_6_X, REDIS_5_0, REDIS_4_0 and REDIS_3_2.
data_type: string
update_allowed: false
hidden: true
required_apis:
- redis.googleapis.com
- vpcaccess.googleapis.com
- integration_type: firestore
resource_type: firestore
label: 'Firestore'
product: Firestore
description: Configure a Firestore database and connect it to a Cloud Run Service.
example_command: |-
$ gcloud {track} run integrations create --service=[SERVICE] --type=firestore
service_type: backing
visible: true
eta_in_min: 5
cta: >-
To connect to the Firestore Database utilize the environment variables FIRESTORE_DB_NAME.
These have been added to the Cloud Run service for you.
parameters: []
required_apis:
- firestore.googleapis.com
- integration_type: cloudsql
resource_type: cloudsql
label: Cloud SQL
product: Cloud SQL
description: Configure a CloudSQL database instance and connect it to a Cloud Run Service.
example_command: |-
$ gcloud {track} run integrations create --service=[SERVICE] --type=cloudsql --parameters=version=MYSQL_8_0
example_yaml: |
apiVersion: runapps.googleapis.com/v1alpha1
resources:
- id: cloudsql/cloudsql-1
config:
version: POSTGRES_11
- id: service/[SERVICE]
bindings:
- target: cloudsql/cloudsql-1
service_type: backing
visible: false
eta_in_min: 15
cta: >-
To connect to the CloudSQL instance utilize the environment variables DB_NAME, INSTANCE_HOST,
DB_USER, and DB_PASS. These have been added to the Cloud Run service for you.
parameters:
- name: version
label: Database Version
description: >-
The version of CloudSQL software. For example: MYSQL_8_0, POSTGRES_14, or SQLSERVER_2019_STANDARD.
See https://cloud.google.com/sql/docs/mysql/admin-api/rest/v1beta4/SqlDatabaseVersion for more details.
data_type: string
update_allowed: false
required: true
required_apis:
- sqladmin.googleapis.com
- cloudresourcemanager.googleapis.com
- secretmanager.googleapis.com
- integration_type: firebase-hosting
resource_type: firebase-hosting
label: Firebase Hosting
product: Firebase Hosting
description: Configure custom domains for Cloud Run services with Firebase Hosting.
example_command: |-
$ gcloud {track} run integrations create --service=[SERVICE] --type=firebase-hosting --parameters=site-id=examplesite
example_yaml: |
apiVersion: runapps.googleapis.com/v1alpha1
resources:
- id: firebase-hosting/firebase-hosting-1
config:
siteId: examplesite
bindings:
- target: service/cowsay
service_type: ingress
visible: true
eta_in_min: 5
cta: |-
To configure free custom domain mappings for this site, visit the Firebase console at https://console.firebase.google.com/project/%%project%%/hosting/sites/%%config.siteId%%
To make this site publicly available, make sure the Cloud Run service has ingress configured to allow 'All' traffic. Learn more at https://cloud.google.com/run/docs/securing/ingress
parameters:
- name: site-id
config_name: siteId
label: Subdomain (Site ID)
description: 'The name of the Firebase Hosting site, which is the sub-domain of the default firebase domains created.'
data_type: string
update_allowed: true
required: true
required_apis:
- firebasehosting.googleapis.com
- integration_type: service
resource_type: service
label: Cloud Run Service
product: Cloud Run Service
description: Configure a Cloud Run service.
example_command: ""
example_yaml: |
apiVersion: runapps.googleapis.com/v1alpha1
resources:
- id: service/myapp
config:
containers:
- image: us-docker.pkg.dev/cloudrun/container/hello
service_type: workload
visible: true
eta_in_min: 5
parameters:
- name: image
label: Container Image
description: 'The container image to use to deploy the Cloud Run service.'
data_type: string
update_allowed: true
required: false
required_apis:
- run.googleapis.com
- integration_type: job
resource_type: job
label: Cloud Run Job
product: Cloud Run Job
description: Configure a Cloud Run job.
example_command: ""
example_yaml: |
apiVersion: runapps.googleapis.com/v1alpha1
resources:
- id: job/myjob
config:
containers:
- image: us-docker.pkg.dev/cloudrun/container/job
service_type: workload
visible: false
eta_in_min: 5
parameters:
- name: image
label: Container Image
description: 'The container image to use to deploy the Cloud Run job.'
data_type: string
update_allowed: true
required: false
required_apis:
- run.googleapis.com
- integration_type: vertex-genai
resource_type: vertex-genai
label: Vertex AI - Generative AI
product: Vertex AI - Generative AI
description: Configure access to Gemini, PaLM, Codey and more of Google's large generative models from your Cloud Run workloads
example_command: |-
$ gcloud {track} run integrations create --service=[SERVICE] --type=vertex-genai
example_yaml: |
apiVersion: runapps.googleapis.com/v1alpha1
resources:
- id: vertex-genai/gemini
service_type: backing
visible: true
eta_in_min: 5
cta: |-
The Vertex AI User (roles/aiplatform.user) IAM role has been added to the service acount of your service.
You can explore how to use Vertex Generative AI in your application code here: https://cloud.google.com/vertex-ai/docs/generative-ai/start/quickstarts/quickstart-multimodal
You can also use the Generative AI Studio to explore: https://console.cloud.google.com/vertex-ai/generative
parameters: []
required_apis:
- aiplatform.googleapis.com

View File

@@ -0,0 +1,305 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""Functionality related to Cloud Run Integration API clients."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os
from typing import List, Optional
from googlecloudsdk.command_lib.runapps import exceptions
from googlecloudsdk.core import properties
from googlecloudsdk.core import yaml
from googlecloudsdk.generated_clients.apis.runapps.v1alpha1 import runapps_v1alpha1_client
from googlecloudsdk.generated_clients.apis.runapps.v1alpha1 import runapps_v1alpha1_messages
BASELINE_APIS = ('runapps.googleapis.com',)
LATEST_DEPLOYMENT_FIELD = 'latestDeployment'
SERVICE_TYPE = 'service'
_TYPE_METADATA = None
class UpdateExclusiveGroup:
def __init__(self, params, required=False):
self.params = params
self.required = required
class ServiceType:
"""Types of services supported by runapps."""
BACKING = 'backing'
INGRESS = 'ingress'
WORKLOAD = 'workload'
def _ServiceTypeFromStr(s: str) -> ServiceType:
"""Converts string into service type."""
types = {
'backing': ServiceType.BACKING,
'ingress': ServiceType.INGRESS,
'workload': ServiceType.WORKLOAD,
}
service_type = types.get(s.lower(), None)
if service_type is None:
raise exceptions.ArgumentError('Service type {} is not supported'.format(s))
return service_type
class Parameters:
"""Each integration has a list of parameters that are stored in this class.
Attributes:
name: Name of the parameter.
description: Explanation of the parameter that is visible to the
customer.
data_type: Denotes what values are acceptable for the parameter.
update_allowed: If false, the param can not be provided in an update
command.
required: If true, the param must be provided on a create command.
hidden: If true, the param will not show up in error messages, but can
be provided by the user.
create_allowed: If false, the param cannot be provided on a create
command.
default: The value provided for the param if the user has not provided one.
config_name: The name of the associated field in the config. If not
provided, it will default to camelcase of `name`.
label: The descriptive name of the param.
"""
def __init__(self, name: str, description: str, data_type: str,
update_allowed: bool = True,
required: bool = False,
hidden: bool = False,
create_allowed: bool = True,
default: Optional[object] = None,
config_name: Optional[str] = None,
label: Optional[str] = None,
):
self.name = name
self.config_name = config_name if config_name else ToCamelCase(name)
self.description = description
self.data_type = data_type
self.update_allowed = update_allowed
self.required = required
self.hidden = hidden
self.create_allowed = create_allowed
self.default = default
self.label = label
class TypeMetadata:
"""Metadata for each integration type supported by Runapps.
Attributes:
integration_type: Name of integration type.
resource_type: Name of resource type.
description: Description of the integration that is visible to the user.
example_command: Example commands that will be provided to the user.
required_field: Field that must exist in the resource config.
service_type: Denotes what type of service the integration is.
parameters: What users can provide for the given integration.
update_exclusive_groups: A list of groups, where each group contains
parameters that cannot be provided at the same time. Only one in the set
can be provided by the user for each command.
disable_service_flags: If true, the --service flag cannot be provided.
singleton_name: If this field is provided, then the integration can only be
a singleton. The name is used as an identifier in the resource config.
required_apis: APIs required for the integration to work. The user will be
prompted to enable these APIs if they are not already enabled.
eta_in_min: estimate deploy time in minutes.
cta: call to action template.
label: the display name for the integration.
product: the GCP product behind the integration.
example_yaml: Example yaml blocks that will be provided to the user.
visible: If true, then the integration is useable by anyone without any
special configuration.
"""
def __init__(self, integration_type: str, resource_type: str,
description: str, example_command: str,
service_type: ServiceType, required_apis: List[str],
parameters: List[Parameters],
update_exclusive_groups:
Optional[List[UpdateExclusiveGroup]] = None,
disable_service_flags: bool = False,
singleton_name: Optional[str] = None,
required_field: Optional[str] = None,
eta_in_min: Optional[int] = None,
cta: Optional[str] = None,
label: Optional[str] = None,
product: Optional[str] = None,
example_yaml: Optional[str] = None,
visible: bool = False):
self.integration_type = integration_type
self.resource_type = resource_type
self.description = description
self.example_command = example_command
self.service_type = _ServiceTypeFromStr(service_type)
self.required_apis = set(required_apis)
self.parameters = [Parameters(**param) for param in parameters]
self.disable_service_flags = disable_service_flags
self.singleton_name = singleton_name
self.required_field = required_field
self.eta_in_min = eta_in_min
self.cta = cta
self.label = label
self.product = product
self.example_yaml = example_yaml
self.visible = visible
if update_exclusive_groups is None:
update_exclusive_groups = []
self.update_exclusive_groups = [
UpdateExclusiveGroup(**group) for group in update_exclusive_groups]
def _GetAllTypeMetadata() -> List[TypeMetadata]:
"""Returns metadata for each integration type.
This loads the metadata from a yaml file at most once and will return the
same data stored in memory upon future calls.
Returns:
array, the type metadata list
"""
global _TYPE_METADATA
if _TYPE_METADATA is None:
dirname = os.path.dirname(__file__)
filename = os.path.join(dirname, 'metadata.yaml')
metadata = yaml.load_path(filename)
_TYPE_METADATA = [
TypeMetadata(**integ) for integ in metadata['integrations']
]
return _TYPE_METADATA
def IntegrationTypes(client: runapps_v1alpha1_client) -> List[TypeMetadata]:
"""Gets the type definitions for Cloud Run Integrations.
Currently it's just returning some builtin defnitions because the API is
not implemented yet.
Args:
client: The api client to use.
Returns:
array of integration type.
"""
del client
return [
integration for integration in _GetAllTypeMetadata()
if _IntegrationVisible(integration)
]
def GetTypeMetadata(integration_type: str) -> Optional[TypeMetadata]:
"""Returns metadata associated to an integration type.
Args:
integration_type: str
Returns:
If the integration does not exist or is not visible to the user,
then None is returned.
"""
for integration in _GetAllTypeMetadata():
if (integration.integration_type == integration_type and
_IntegrationVisible(integration)):
return integration
return None
def GetTypeMetadataByResourceType(
resource_type: str,
) -> Optional[TypeMetadata]:
"""Returns metadata associated to an integration type.
Args:
resource_type: the resource type
Returns:
If the integration does not exist or is not visible to the user,
then None is returned.
"""
for integration in _GetAllTypeMetadata():
if integration.resource_type == resource_type and _IntegrationVisible(
integration
):
return integration
return None
def GetTypeMetadataByResource(
resource: runapps_v1alpha1_messages.Resource,
) -> Optional[TypeMetadata]:
"""Returns metadata associated to an integration type.
Args:
resource: the resource object
Returns:
If the integration does not exist or is not visible to the user,
then None is returned.
"""
return GetTypeMetadataByResourceType(resource.id.type)
def _IntegrationVisible(integration: TypeMetadata) -> bool:
"""Returns whether or not the integration is visible.
Args:
integration: Each entry is defined in _INTEGRATION_TYPES
Returns:
True if the integration is set to visible, or if the property
is set to true. Otherwise it is False.
"""
show_experimental_integrations = (
properties.VALUES.runapps.experimental_integrations.GetBool())
return integration.visible or show_experimental_integrations
def CheckValidIntegrationType(integration_type: str) -> None:
"""Checks if IntegrationType is supported.
Args:
integration_type: integration type to validate.
Rasies: ArgumentError
"""
if GetTypeMetadata(integration_type) is None:
raise exceptions.ArgumentError(
'Integration of type {} is not supported'.format(integration_type))
def ToCamelCase(name: str) -> str:
"""Turns a kebab case name into camel case.
Args:
name: the name string
Returns:
the string in camel case
"""
pascal_case = name.replace('-', ' ').title().replace(' ', '')
return pascal_case[0].lower() + pascal_case[1:]

View File

@@ -0,0 +1,304 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 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.
"""Used to validate integrations are setup correctly for deployment."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from typing import Dict, List
from googlecloudsdk.api_lib.run.integrations import types_utils
from googlecloudsdk.api_lib.services import enable_api
from googlecloudsdk.api_lib.services import services_util
from googlecloudsdk.api_lib.services import serviceusage
from googlecloudsdk.command_lib.runapps import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_io
_API_ENABLEMENT_CONFIRMATION_TEXT = {
'firebasehosting.googleapis.com': (
'By enabling the Firebase Hosting API you are agreeing to the Firebase'
' Terms of Service. Learn more at https://firebase.google.com/terms'
)
}
def GetIntegrationValidator(integration_type: str):
"""Gets the integration validator based on the integration type."""
type_metadata = types_utils.GetTypeMetadata(integration_type)
if type_metadata is None:
raise ValueError(
'Integration type: [{}] has not been defined in types_utils'
.format(integration_type))
return Validator(type_metadata)
def _ConstructPrompt(apis_not_enabled: List[str]) -> str:
"""Returns a prompt to enable APIs with any custom text per-API.
Args:
apis_not_enabled: APIs that are to be enabled.
Returns: prompt string to be displayed for confirmation.
"""
if not apis_not_enabled:
return ''
base_prompt = (
'Do you want to enable these APIs to continue (this will take a few'
' minutes)?'
)
prompt = ''
for api in apis_not_enabled:
if api in _API_ENABLEMENT_CONFIRMATION_TEXT:
prompt += _API_ENABLEMENT_CONFIRMATION_TEXT[api] + '\n'
prompt += base_prompt
return prompt
def EnableApis(apis_not_enabled: List[str], project_id: str):
"""Enables the given API on the given project.
Args:
apis_not_enabled: the apis that needs enablement
project_id: the project ID
"""
apis_to_enable = '\n\t'.join(apis_not_enabled)
console_io.PromptContinue(
default=False,
cancel_on_no=True,
message=(
'The following APIs are not enabled on project [{0}]:\n\t{1}'
.format(project_id, apis_to_enable)
),
prompt_string=_ConstructPrompt(apis_not_enabled),
)
log.status.Print(
'Enabling APIs on project [{0}]...'.format(project_id))
op = serviceusage.BatchEnableApiCall(project_id, apis_not_enabled)
if not op.done:
op = services_util.WaitOperation(op.name, serviceusage.GetOperation)
services_util.PrintOperation(op)
def CheckApiEnablements(types: List[str]):
"""Checks if all GCP APIs required by the given types are enabled.
If some required APIs are not enabled, it will prompt the user to enable them.
If they do not want to enable them, the process will exit.
Args:
types: list of types to check.
"""
project_id = properties.VALUES.core.project.Get()
apis_not_enabled = []
for typekit in types:
try:
validator = GetIntegrationValidator(typekit)
apis = validator.GetDisabledGcpApis(project_id)
if apis:
apis_not_enabled.extend(apis)
except ValueError:
continue
if apis_not_enabled:
EnableApis(apis_not_enabled, project_id)
class Validator:
"""Validates an integration is setup correctly for deployment."""
def __init__(self, type_metadata: types_utils.TypeMetadata):
self.type_metadata = type_metadata
def ValidateEnabledGcpApis(self):
"""Validates user has all GCP APIs enabled for an integration.
If the user does not have all the GCP APIs enabled they will
be prompted to enable them. If they do not want to enable them,
then the process will exit.
"""
project_id = properties.VALUES.core.project.Get()
apis_not_enabled = self.GetDisabledGcpApis(project_id)
if apis_not_enabled:
EnableApis(apis_not_enabled, project_id)
def GetDisabledGcpApis(self, project_id: str) -> List[str]:
"""Returns all GCP APIs needed for an integration.
Args:
project_id: The project's ID
Returns:
A list where each item is a GCP API that is not enabled.
"""
required_apis = set(self.type_metadata.required_apis).union(
types_utils.BASELINE_APIS
)
project_id = properties.VALUES.core.project.Get()
apis_not_enabled = [
# iterable is sorted for scenario tests. The order of API calls
# should happen in the same order each time for the scenario tests.
api
for api in sorted(required_apis)
if not enable_api.IsServiceEnabled(project_id, api)
]
return apis_not_enabled
def ValidateCreateParameters(self, parameters: Dict[str, str], service: str):
"""Validates parameters provided for creating an integration.
Three things are done for all integrations created:
1. Check that parameters passed in are valid (exist in types_utils
mapping) and are not misspelled. These are parameters that will
be recognized by the control plane.
2. Check that all required parameters are provided.
3. Check that default values are set for parameters
that are not provided.
Note that user provided params may be modified in place
if default values are missing.
Args:
parameters: A dict where the key, value mapping is provided by the user.
service: The service to bind to the new integration.
"""
self._ValidateProvidedParams(parameters)
self._CheckServiceFlag(service, required=True)
self._CheckForInvalidCreateParameters(parameters)
self._ValidateRequiredParams(parameters)
self._SetupDefaultParams(parameters)
def ValidateUpdateParameters(self, parameters):
"""Checks that certain parameters have not been updated.
This firstly checks that the parameters provided exist in the mapping
and thus are recognized the control plane.
Args:
parameters: A dict where the key, value mapping is provided by the user.
"""
self._ValidateProvidedParams(parameters)
self._CheckForInvalidUpdateParameters(parameters)
def _CheckForInvalidCreateParameters(self, user_provided_params):
"""Raises an exception that lists the parameters that can't be changed."""
invalid_params = []
for param in self.type_metadata.parameters:
if not param.create_allowed and param.name in user_provided_params:
invalid_params.append(param.name)
if invalid_params:
raise exceptions.ArgumentError(
('The following parameters are not allowed in create command: {}')
.format(self._RemoveEncoding(invalid_params))
)
def _CheckForInvalidUpdateParameters(self, user_provided_params):
"""Raises an exception that lists the parameters that can't be changed."""
invalid_params = []
for param in self.type_metadata.parameters:
if not param.update_allowed and param.name in user_provided_params:
invalid_params.append(param.name)
if invalid_params:
raise exceptions.ArgumentError(
('The following parameters: {} cannot be changed once the ' +
'integration has been created')
.format(self._RemoveEncoding(invalid_params))
)
for exclusive_groups in self.type_metadata.update_exclusive_groups:
found = 0
group_params = set(exclusive_groups.params)
# Generate a stable order list of the param for output.
params_list_str = ', '.join(sorted(group_params))
for param_name in group_params:
if param_name in user_provided_params:
found += 1
if found > 1:
raise exceptions.ArgumentError(
('At most one of these parameters can be specified: {}'
).format(params_list_str))
if exclusive_groups.required and found == 0:
raise exceptions.ArgumentError(
('At least one of these parameters must be specified: {}')
.format(params_list_str)
)
def _CheckServiceFlag(self, service, required=False):
"""Raises an exception that lists the parameters that can't be changed."""
disable_service_flags = self.type_metadata.disable_service_flags
if disable_service_flags and service:
raise exceptions.ArgumentError(
('--service not allowed for integration type [{}]'.format(
self.type_metadata.integration_type)))
if not disable_service_flags and not service and required:
raise exceptions.ArgumentError(('--service is required'))
def _ValidateProvidedParams(self, user_provided_params):
"""Checks that the user provided parameters exist in the mapping."""
invalid_params = []
allowed_params = [
param.name for param in self.type_metadata.parameters
]
for param in user_provided_params:
if param not in allowed_params:
invalid_params.append(param)
if invalid_params:
raise exceptions.ArgumentError(
'The following parameters: {} are not allowed'.format(
self._RemoveEncoding(invalid_params))
)
def _ValidateRequiredParams(self, user_provided_params):
"""Checks that required parameters are provided by the user."""
missing_required_params = []
for param in self.type_metadata.parameters:
if param.required and param.name not in user_provided_params:
missing_required_params.append(param.name)
if missing_required_params:
raise exceptions.ArgumentError(
('The following parameters: {} are required to create an ' +
'integration of type [{}]').format(
self._RemoveEncoding(missing_required_params),
self.type_metadata.integration_type))
def _RemoveEncoding(self, elements):
"""Removes encoding for each element in the list.
This causes inconsistencies in the scenario test when the output
looks like [u'domain'] instead of ['domain']
Args:
elements: list
Returns:
list[str], encoding removed from each element.
"""
return [str(x) for x in elements]
def _SetupDefaultParams(self, user_provided_params):
"""Ensures that default parameters have a value if not set."""
for param in self.type_metadata.parameters:
if param.default and param.name not in user_provided_params:
user_provided_params[param.name] = param.default