File: //lib/google-cloud-sdk/lib/googlecloudsdk/command_lib/game/servers/utils.py
# -*- coding: utf-8 -*- #
# Copyright 2019 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.
"""Utility functions for Cloud Game Servers update commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
from apitools.base.protorpclite import messages as _messages
from apitools.base.py import encoding
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.api_lib.util import waiter
from googlecloudsdk.calliope import base
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import resources
from googlecloudsdk.core import yaml
import ruamel.yaml as ryaml
import six
GAME_SERVICES_API = 'gameservices'
OPERATIONS_COLLECTION = 'gameservices.projects.locations.operations'
def GetApiMessage(api_version):
return apis.GetMessagesModule(GAME_SERVICES_API, api_version)
def GetClient(api_version):
return apis.GetClientInstance(GAME_SERVICES_API, api_version)
def GetApiVersionFromArgs(args):
"""Return API version based on args.
Update this whenever there is a new version.
Args:
args: The argparse namespace.
Returns:
API version (e.g. v1alpha or v1beta).
Raises:
UnsupportedReleaseTrackError: If invalid release track from args.
"""
release_track = args.calliope_command.ReleaseTrack()
if release_track == base.ReleaseTrack.ALPHA:
return 'v1alpha'
if release_track == base.ReleaseTrack.BETA:
return 'v1beta'
if release_track == base.ReleaseTrack.GA:
return 'v1'
raise UnsupportedReleaseTrackError(release_track)
class UnsupportedReleaseTrackError(Exception):
"""Raised when requesting an api for an unsupported release track."""
def ParseClusters(api_version, key, val, messages=None):
messages = messages or GetApiMessage(api_version)
return messages.LabelSelector.LabelsValue.AdditionalProperty(
key=key, value=val)
def ParseLabels(api_version, cluster_labels, messages=None):
messages = messages or GetApiMessage(api_version)
selectors = messages.LabelSelector.LabelsValue()
selectors.additionalProperties = cluster_labels
label_selector = messages.LabelSelector()
label_selector.labels = selectors
return label_selector
def GetMessages(api_version):
return apis.GetMessagesModule(GAME_SERVICES_API, api_version)
def WaitForOperation(response, api_version):
operation_ref = resources.REGISTRY.ParseRelativeName(
response.name, collection=OPERATIONS_COLLECTION)
return waiter.WaitFor(
waiter.CloudOperationPollerNoResources(
GetClient(api_version).projects_locations_operations), operation_ref,
'Waiting for [{0}] to finish'.format(operation_ref.Name()))
class InvalidSpecFileError(exceptions.Error):
"""Error if a spec file is not valid JSON or YAML."""
class InvalidSchemaError(exceptions.Error):
"""Error if a schema is improperly specified."""
def ProcessConfigOverrideFile(config_override_file, api_version):
"""Reads a JSON/YAML config_override_file and returns collection of config override object."""
try:
overrides = json.loads(config_override_file)
except ValueError as e:
try:
overrides = yaml.load(config_override_file)
except yaml.YAMLParseError as e:
raise InvalidSpecFileError(
'Error parsing config_override file: [{}]'.format(e))
messages = GetMessages(api_version)
message_class = messages.GameServerConfigOverride
try:
overrides_messages = [
encoding.DictToMessage(o, message_class) for o in overrides
]
except AttributeError:
raise InvalidSchemaError(
'Invalid schema: unexpected game server config override(s) format.')
except _messages.ValidationError as e:
# Unfortunately apitools doesn't provide a way to get the path to the
# invalid field here.
raise InvalidSchemaError('Invalid schema: [{}]'.format(e))
for msg in overrides_messages:
_CheckUnrecognizedFieldPaths(msg)
return overrides_messages
def ProcessFleetConfigsFile(fleet_configs_file, api_version):
"""Reads a JSON/YAML fleet_configs_file and returns collectiong of fleet configs object."""
try:
fleet_configs = json.loads(fleet_configs_file)
except ValueError as e:
try:
fleet_configs = yaml.load(fleet_configs_file)
except yaml.YAMLParseError as e:
raise InvalidSpecFileError(
'Error parsing fleet_configs file: [{}]'.format(e))
messages = GetMessages(api_version)
message_class = messages.FleetConfig
fleet_configs_messages = []
try:
for fc in fleet_configs:
f = encoding.DictToMessage(fc, message_class)
spec = yaml.load(f.fleetSpec)
spec_as_json_str = json.dumps(spec)
f.fleetSpec = spec_as_json_str
fleet_configs_messages.append(f)
except AttributeError:
raise InvalidSchemaError('Invalid schema: expected proper fleet configs')
except ryaml.error.YAMLStreamError:
raise InvalidSchemaError(
'Invalid schema: expect `fleetSpec:` in fleet configs.')
except _messages.ValidationError as e:
# The most likely reason this is raised is the file that is submitted is
# following new format (json/yaml without string blob) and we will parse
# with the new format
for fc in fleet_configs:
f = messages.FleetConfig()
if 'name' in fc:
f.name = fc['name']
spec_as_json_str = json.dumps(fc['fleetSpec'])
f.fleetSpec = spec_as_json_str
fleet_configs_messages.append(f)
for msg in fleet_configs_messages:
_CheckUnrecognizedFieldPaths(msg)
return fleet_configs_messages
def ProcessScalingConfigsFile(scaling_configs_file, api_version):
"""Reads a JSON/YAML scaling_configs_file and returns collectiong of scaling configs object."""
try:
scaling_configs = json.loads(scaling_configs_file)
except ValueError as e:
try:
scaling_configs = yaml.load(scaling_configs_file)
except yaml.YAMLParseError as e:
raise InvalidSpecFileError(
'Error parsing scaling_configs file: [{}]'.format(e))
messages = GetMessages(api_version)
message_class = messages.ScalingConfig
selector = messages.LabelSelector()
scaling_configs_messages = []
try:
for sc in scaling_configs:
esc = encoding.DictToMessage(sc, message_class)
if not esc.selectors:
# Add default selector if not set
esc.selectors = [selector]
# Convert yaml to json
spec = yaml.load(esc.fleetAutoscalerSpec)
spec_as_json_str = json.dumps(spec)
esc.fleetAutoscalerSpec = spec_as_json_str
scaling_configs_messages.append(esc)
except AttributeError:
raise InvalidSchemaError('Invalid schema: expected proper scaling configs')
except ryaml.error.YAMLStreamError:
raise InvalidSchemaError(
'Invalid schema: expect `fleetAutoscalerSpec:` in scaling configs.')
except _messages.ValidationError as e:
# The most likely reason this is reaised is the file that is submitted is
# following new format (json/yaml without string blob) and we will parse
# with the new format
for sc in scaling_configs:
s = messages.ScalingConfig()
if 'selectors' in sc:
if not isinstance(sc['selectors'], list):
raise InvalidSchemaError('Invalid schema: selectors must be a list')
s.selectors = []
for lab in sc['selectors']:
labels = []
for (key, val) in lab['labels'].items():
labels.append(ParseClusters(None, key, val, messages))
s.selectors.append(ParseLabels(None, labels, messages))
else:
# Add default selector if not set
s.selectors = [selector]
if 'name' in sc:
s.name = sc['name']
if 'schedules' in sc:
s.schedules = []
if not isinstance(sc['schedules'], list):
raise InvalidSchemaError('Invalid schema: schedules must be a list')
for sh in sc['schedules']:
schedule = messages.Schedule()
if 'cronJobDuration' in sh:
schedule.cronJobDuration = sh['cronJobDuration']
if 'cronSpec' in sh:
schedule.cronSpec = sh['cronSpec']
if 'startTime' in sh:
schedule.startTime = sh['startTime']
if 'endTime' in sh:
schedule.endTime = sh['endTime']
s.schedules.append(schedule)
spec_as_json_str = json.dumps(sc['fleetAutoscalerSpec'])
s.fleetAutoscalerSpec = spec_as_json_str
scaling_configs_messages.append(s)
for msg in scaling_configs_messages:
_CheckUnrecognizedFieldPaths(msg)
return scaling_configs_messages
def _CheckUnrecognizedFieldPaths(message):
"""Raise error if unrecognized fields found in the message."""
errors = encoding.UnrecognizedFieldIter(message)
unrecognized_field_paths = []
for edges_to_message, field_names in errors:
message_field_path = '.'.join(six.text_type(e) for e in edges_to_message)
# Don't print the top level columns field since the user didn't specify it
message_field_path = message_field_path.replace('columns', '', 1)
for field_name in field_names:
unrecognized_field_paths.append('{}.{}'.format(message_field_path,
field_name))
unrecognized_field_paths = sorted(unrecognized_field_paths)
if unrecognized_field_paths:
error_msg_lines = [
'Invalid schema, the following fields are unrecognized:'] + \
unrecognized_field_paths
raise InvalidSchemaError('\n'.join(error_msg_lines))