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,399 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Command to create SaaS Runtime Flags from a manifest file."""
from __future__ import annotations
import json
import pprint
from typing import Any, Callable
from apitools.base.protorpclite import messages as rpclite_messages
from apitools.base.py import encoding
from apitools.base.py import exceptions
import googlecloudsdk.api_lib.saasservicemgmt.util as saasservicemgmt_util
from googlecloudsdk.api_lib.util import messages as msg_utils
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.command_lib.saas_runtime import flags as arg_utils
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.generated_clients.apis.saasservicemgmt.v1beta1 import saasservicemgmt_v1beta1_client as ezclient
from googlecloudsdk.generated_clients.apis.saasservicemgmt.v1beta1 import saasservicemgmt_v1beta1_messages as ezmessages
FLAG_TYPE_MAP: dict[str, str] = {
'boolean': 'FLAG_VALUE_TYPE_BOOL',
'integer': 'FLAG_VALUE_TYPE_INT',
'double': 'FLAG_VALUE_TYPE_DOUBLE',
'string': 'FLAG_VALUE_TYPE_STRING',
}
VALUE_TYPE_HANDLERS: dict[str, tuple[str, type[Any]]] = {
'FLAG_VALUE_TYPE_BOOL': ('boolValue', bool),
'FLAG_VALUE_TYPE_INT': ('intValue', int),
'FLAG_VALUE_TYPE_DOUBLE': ('doubleValue', float),
'FLAG_VALUE_TYPE_STRING': ('stringValue', str),
}
def _ValidFlagType(flag_type: str | None) -> bool:
"""Returns True if the flag type is valid."""
return flag_type is not None and flag_type in FLAG_TYPE_MAP
def _ParseFlagData(
flag_key: str,
unit_kind_name: str,
flag_definition: dict[str, Any],
) -> dict[str, Any]:
"""Parses a flag definition and returns a dict of fields to be set."""
log.debug(f'Flag definition:\n{pprint.pformat(flag_definition)}\n')
flag_type = flag_definition.get('flagType')
if not _ValidFlagType(flag_type):
raise exceptions.InvalidDataError(
f'Flag "{flag_key}" invalid. "{flag_type}" is not a supported flag'
' type.'
)
value_type = FLAG_TYPE_MAP.get(flag_type)
default_value = flag_definition.get('defaultValue')
if default_value is None:
raise exceptions.InvalidDataError(
f'Flag "{flag_key}" invalid. "defaultValue" is not set.'
)
flag_dict = {
'key': flag_key,
'valueType': value_type,
'evaluationSpec': _GetDefaultEvaluationSpecDict(
default_value, value_type
),
'unitKind': unit_kind_name,
}
if flag_definition.get('description'):
flag_dict['description'] = flag_definition['description']
return flag_dict
def _GetDefaultEvaluationSpecDict(
default_value: str, value_type: str
) -> dict[str, Any]:
"""Returns the default evaluation spec for the given value type."""
if value_type not in VALUE_TYPE_HANDLERS:
raise exceptions.InvalidDataError(f'Unsupported value type: {value_type}')
variant_type, expected_type = VALUE_TYPE_HANDLERS[
value_type
]
if not isinstance(default_value, expected_type):
raise exceptions.InvalidDataError(
f'Invalid default value "{default_value}" for type {value_type}.'
f' Expected type {expected_type.__name__} but got'
f' {type(default_value).__name__}.'
)
variant_dict = {
'name': 'default',
}
variant_dict[variant_type] = expected_type(default_value)
return {
'defaultTarget': 'default',
'variants': [variant_dict],
}
def _GetFlagMessagesFromManifest(
manifest_json: dict[str, Any], unit_kind_name: str
) -> list[ezmessages.Flag]:
"""Parses a JSON manifest file and returns a list of flags."""
if 'flags' not in manifest_json:
raise exceptions.InvalidDataError(
'Manifest must have a top-level "flags" key.'
)
flags_data = manifest_json['flags']
if not isinstance(flags_data, dict):
raise exceptions.InvalidDataError('"flags" property is invalid.')
has_error = False
flags = []
for flag_key, flag_definition in flags_data.items():
try:
flag_dict = _ParseFlagData(flag_key, unit_kind_name, flag_definition)
flag_msg = msg_utils.DictToMessageWithErrorCheck(
flag_dict, ezmessages.Flag, throw_on_unexpected_fields=False
)
flags.append(flag_msg)
except (
rpclite_messages.ValidationError,
exceptions.InvalidDataError,
) as e:
log.error(f'Failed to parse flag definition for flag "{flag_key}": {e!r}')
has_error = True
if has_error:
raise exceptions.InvalidDataError(
'Failed to parse some flags in the manifest.'
)
return flags
def _GetValidFlags(
flags: list[ezmessages.Flag],
flags_service: ezclient.SaasservicemgmtV1beta1.ProjectsLocationsFlagsService,
parent: str,
) -> list[ezmessages.Flag]:
"""Returns a list of flags that are valid to create."""
valid_flags = []
validation_failed = False
for flag_msg in flags:
try:
_CreateFlag(flags_service, parent, flag_msg, validate_only=True)
except exceptions.HttpConflictError:
log.status.Print(f'Flag {flag_msg.key} already exists. Ignoring.')
except exceptions.HttpError as e:
_LogHttpError(e, flag_msg.key, log.error, 'Flag creation would fail')
validation_failed = True
except exceptions.InvalidDataError as e:
log.error(f'Flag creation would fail: {e}')
validation_failed = True
else:
valid_flags.append(flag_msg)
if validation_failed:
raise exceptions.InvalidDataError('Manifest file contains invalid flags')
return valid_flags
def _IsFlagAlreadyExistsError(e: exceptions.HttpError) -> bool:
"""Returns True if the error is due to a flag already existing."""
if not isinstance(e, exceptions.HttpError):
return False
if e.status_code != 400:
return False
try:
error_content = json.loads(e.content)
error_message = error_content.get('error', {}).get('message', '')
return 'already exists' in error_message
except json.JSONDecodeError:
return False
def _GetExistingFlag(
flags_service: ezclient.SaasservicemgmtV1beta1.ProjectsLocationsFlagsService,
parent: str,
flag_key: str,
) -> ezmessages.Flag | None:
"""Gets a flag by its key."""
try:
get_request = ezmessages.SaasservicemgmtProjectsLocationsFlagsGetRequest(
name=f'{parent}/flags/{flag_key}'
)
return flags_service.Get(get_request)
except exceptions.HttpError as e:
_LogHttpError(e, flag_key, log.error, 'Failed to get existing flag')
return None
def _CreateFlag(
flags_service: ezclient.SaasservicemgmtV1beta1.ProjectsLocationsFlagsService,
parent: str,
flag_msg: ezmessages.Flag,
validate_only: bool = False,
) -> None:
"""Creates a flag in the SaaS Runtime API."""
flags_create_req = (
ezmessages.SaasservicemgmtProjectsLocationsFlagsCreateRequest(
parent=parent,
flagId=flag_msg.key,
validateOnly=validate_only,
flag=flag_msg,
)
)
flags_create_req_dict = encoding.MessageToDict(flags_create_req)
if not validate_only:
log.status.Print(
f'\nAttempting to create flag:\n{pprint.pformat(flags_create_req_dict)}'
)
try:
# Note: base_api client has a retry mechanism built in.
flag_resp = flags_service.Create(flags_create_req)
if not validate_only:
log.status.Print(
'\nCreated flag:\n'
f'{pprint.pformat(encoding.MessageToDict(flag_resp))}'
)
except exceptions.HttpError as e:
if _IsFlagAlreadyExistsError(e):
existing_flag = _GetExistingFlag(flags_service, parent, flag_msg.key)
if existing_flag is None:
raise
log.debug(
f'\nFlag {flag_msg.key} already exists. Existing'
f' flag:\n{pprint.pformat(encoding.MessageToDict(existing_flag))}'
)
if existing_flag.valueType != flag_msg.valueType:
raise exceptions.InvalidDataError(
f'Flag {flag_msg.key} of type {flag_msg.valueType} already exists'
f' with different value type {existing_flag.valueType}'
) from e
else:
raise exceptions.HttpConflictError(
e.response, e.content, e.url, e.method_config, e.request
)
else:
raise e
def _LogHttpError(
e: exceptions.HttpError,
flag_key: str,
log_func: Callable[[str], None],
msg_prefix: str = '',
):
"""Logs an HTTP error."""
try:
error_content = json.loads(e.content)
error_message = error_content.get('error', {}).get(
'message', 'Unknown error'
)
error_code = error_content.get('error', {}).get('code', 'Unknown error')
log_func(f'{msg_prefix} {flag_key}: {error_code} : {error_message}')
except json.JSONDecodeError:
log_func(f'{msg_prefix} {flag_key}: {e.content}')
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
@base.Hidden
class Push(base.Command):
"""Create SaaS Runtime Flags from a JSON manifest file."""
detailed_help = {
'brief': 'Create Flags from a JSON manifest file.',
'DESCRIPTION': """\
Creates Flags based on the contents of a provided JSON manifest file. Flags defined in the file
will be created. If a Flag already exists with the same type, it will be ignored. Otherwise,
the command will exit with an error.
""",
'EXAMPLES': """\
To create flags from a manifest file named `my_flags.json`:
$ {command} --file=my_flags.json --unit-kind=my-unit-kind
""",
}
@staticmethod
def Args(parser):
"""Register flags for gcloud saas-runtime flags push: --file, --unit-kind."""
parser.add_argument(
'--file',
type=arg_parsers.FileContents(),
required=True,
help=(
'Path to the JSON manifest file containing the flag definitions.'
' The manifest file must conform to the OpenFeature CLI flag'
' manifest schema.'
),
)
# This also implicitly adds the --location flag.
arg_utils.AddUnitKindArgToParser(
parser,
required=True,
help_text='UnitKind to push all the flags in the manifest file to.',
)
def Run(self, args):
"""Execute the gcloud saas-runtime flags push command."""
project = properties.VALUES.core.project.Get()
unit_kind_ref = args.CONCEPTS.unit_kind.Parse()
parent = unit_kind_ref.Parent().RelativeName()
log.debug(f'--project={project}')
log.debug(f'--location={args.location}')
log.debug(f'--unit-kind={args.unit_kind}')
log.debug(f'Parent reference: {unit_kind_ref.Parent()}')
log.debug(f'Parent relative name: {parent}')
log.debug(f'UnitKind Name: {unit_kind_ref.Name()}')
log.debug(f'UnitKind RelativeName: {unit_kind_ref.RelativeName()}')
client = saasservicemgmt_util.GetV1Beta1ClientInstance()
flags_service = client.projects_locations_flags
# Parse the manifest file.
try:
manifest = json.loads(args.file)
except json.JSONDecodeError as e:
raise calliope_exceptions.ToolException(
f'Failed to parse JSON: {e}'
) from e
# Validate the manifest file.
try:
flags = _GetFlagMessagesFromManifest(
manifest, unit_kind_ref.RelativeName()
)
except exceptions.InvalidDataError as e:
raise calliope_exceptions.ToolException(
f'Failed to parse flags: {e}'
) from e
# Validate the flags.
log.status.Print(f'Found {len(flags)} flags in manifest. Validating...')
try:
valid_flags = _GetValidFlags(flags, flags_service, parent)
except exceptions.InvalidDataError as e:
raise calliope_exceptions.ToolException(
f'No flags were created: {e}'
) from e
# Create the flags.
if not valid_flags:
log.status.Print('No valid flags to create. No flags were created.')
return
log.status.Print(
'All flags validated successfully. Creating'
f' {len(valid_flags)} flags...'
)
for flag_msg in valid_flags:
try:
_CreateFlag(flags_service, parent, flag_msg, validate_only=False)
except exceptions.HttpError as e:
_LogHttpError(e, flag_msg.key, log.error, 'Failed to create flag')
raise calliope_exceptions.ToolException(
f'Halting execution due to post-validation error: {e}'
) from e
log.status.Print(f'{len(valid_flags)} flags created successfully.')