File: //snap/google-cloud-cli/current/lib/surface/compute/backend_services/edit.py
# -*- coding: utf-8 -*- #
# Copyright 2014 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 for modifying backend services."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import io
from apitools.base.protorpclite import messages
from apitools.base.py import encoding
from googlecloudsdk.api_lib.compute import base_classes
from googlecloudsdk.api_lib.compute import property_selector
from googlecloudsdk.calliope import base
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.command_lib.compute import exceptions
from googlecloudsdk.command_lib.compute import flags as compute_flags
from googlecloudsdk.command_lib.compute.backend_services import backend_services_utils
from googlecloudsdk.command_lib.compute.backend_services import flags
from googlecloudsdk.core import resources
from googlecloudsdk.core import yaml
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.util import edit
import six
class InvalidResourceError(calliope_exceptions.ToolException):
# Normally we'd want to subclass core.exceptions.Error, but base_classes.Edit
# abuses ToolException to classify errors when displaying messages to users,
# and we should continue to fit in that framework for now.
pass
class Edit(base.Command):
"""Modify a backend service.
*{command}* modifies a backend service of a Google Cloud load balancer or
Traffic Director. The backend service resource is fetched from the server
and presented in a text editor that displays the configurable fields.
The specific editor is defined by the ``EDITOR'' environment variable.
The name of each backend corresponds to the name of an instance group,
zonal NEG, serverless NEG, or internet NEG.
To add, remove, or swap backends, use the `gcloud compute backend-services
remove-backend` and `gcloud compute backend-services add-backend` commands.
"""
DEFAULT_FORMAT = 'yaml'
_BACKEND_SERVICE_ARG = flags.GLOBAL_REGIONAL_BACKEND_SERVICE_ARG
@classmethod
def Args(cls, parser):
cls._BACKEND_SERVICE_ARG.AddArgument(parser)
def _ProcessEditedResource(self, holder, backend_service_ref, file_contents,
original_object, original_record,
modifiable_record, args):
"""Returns an updated resource that was edited by the user."""
# It's very important that we replace the characters of comment
# lines with spaces instead of removing the comment lines
# entirely. JSON and YAML deserialization give error messages
# containing line, column, and the character offset of where the
# error occurred. If the deserialization fails; we want to make
# sure those numbers map back to what the user actually had in
# front of him or her otherwise the errors will not be very
# useful.
non_comment_lines = '\n'.join(
' ' * len(line) if line.startswith('#') else line
for line in file_contents.splitlines())
modified_record = base_classes.DeserializeValue(
non_comment_lines, args.format or Edit.DEFAULT_FORMAT)
# Normalizes all of the fields that refer to other
# resource. (i.e., translates short names to URIs)
reference_normalizer = property_selector.PropertySelector(
transformations=self.GetReferenceNormalizers(holder.resources))
modified_record = reference_normalizer.Apply(modified_record)
if modifiable_record == modified_record:
new_object = None
else:
modified_record['name'] = original_record['name']
fingerprint = original_record.get('fingerprint')
if fingerprint:
modified_record['fingerprint'] = fingerprint
new_object = encoding.DictToMessage(modified_record,
holder.client.messages.BackendService)
# If existing object is equal to the proposed object or if
# there is no new object, then there is no work to be done, so we
# return the original object.
if not new_object or original_object == new_object:
return [original_object]
return holder.client.MakeRequests(
[self.GetSetRequest(holder.client, backend_service_ref, new_object)])
def Run(self, args):
holder = base_classes.ComputeApiHolder(self.ReleaseTrack())
client = holder.client
backend_service_ref = self._BACKEND_SERVICE_ARG.ResolveAsResource(
args,
holder.resources,
default_scope=backend_services_utils.GetDefaultScope(),
scope_lister=compute_flags.GetDefaultScopeLister(client))
get_request = self.GetGetRequest(client, backend_service_ref)
objects = client.MakeRequests([get_request])
original_object = objects[0]
original_record = encoding.MessageToDict(original_object)
# Selects only the fields that can be modified.
field_selector = property_selector.PropertySelector(properties=[
'backends',
'customRequestHeaders',
'customResponseHeaders',
'description',
'enableCDN',
'healthChecks',
'iap.enabled',
'iap.oauth2ClientId',
'iap.oauth2ClientSecret',
'port',
'portName',
'protocol',
'timeoutSec',
])
modifiable_record = field_selector.Apply(original_record)
file_contents = self.BuildFileContents(args, client, original_record,
modifiable_record)
resource_list = self.EditResource(args, backend_service_ref, file_contents,
holder, modifiable_record,
original_object, original_record)
for resource in resource_list:
yield resource
def BuildFileContents(self, args, client, original_record, modifiable_record):
buf = io.StringIO()
for line in base_classes.HELP.splitlines():
buf.write('#')
if line:
buf.write(' ')
buf.write(line)
buf.write('\n')
buf.write('\n')
buf.write(base_classes.SerializeDict(modifiable_record,
args.format or Edit.DEFAULT_FORMAT))
buf.write('\n')
example = base_classes.SerializeDict(
encoding.MessageToDict(self.GetExampleResource(client)),
args.format or Edit.DEFAULT_FORMAT)
base_classes.WriteResourceInCommentBlock(example, 'Example resource:', buf)
buf.write('#\n')
original = base_classes.SerializeDict(original_record,
args.format or Edit.DEFAULT_FORMAT)
base_classes.WriteResourceInCommentBlock(original, 'Original resource:',
buf)
return buf.getvalue()
def EditResource(self, args, backend_service_ref, file_contents, holder,
modifiable_record, original_object, original_record):
while True:
try:
file_contents = edit.OnlineEdit(file_contents)
except edit.NoSaveException:
raise exceptions.AbortedError('Edit aborted by user.')
try:
resource_list = self._ProcessEditedResource(holder, backend_service_ref,
file_contents,
original_object,
original_record,
modifiable_record, args)
break
except (ValueError, yaml.YAMLParseError,
messages.ValidationError,
calliope_exceptions.ToolException) as e:
message = getattr(e, 'message', six.text_type(e))
if isinstance(e, calliope_exceptions.ToolException):
problem_type = 'applying'
else:
problem_type = 'parsing'
message = ('There was a problem {0} your changes: {1}'
.format(problem_type, message))
if not console_io.PromptContinue(
message=message,
prompt_string='Would you like to edit the resource again?'):
raise exceptions.AbortedError('Edit aborted by user.')
return resource_list
def GetExampleResource(self, client):
uri_prefix = ('https://compute.googleapis.com/compute/v1/projects/'
'my-project/')
instance_groups_uri_prefix = (
'https://compute.googleapis.com/compute/v1/projects/'
'my-project/zones/')
return client.messages.BackendService(
backends=[
client.messages.Backend(
balancingMode=(
client.messages.Backend.BalancingModeValueValuesEnum.RATE),
group=(instance_groups_uri_prefix +
'us-central1-a/instanceGroups/group-1'),
maxRate=100),
client.messages.Backend(
balancingMode=(
client.messages.Backend.BalancingModeValueValuesEnum.RATE),
group=(instance_groups_uri_prefix +
'europe-west1-a/instanceGroups/group-2'),
maxRate=150),
],
customRequestHeaders=['X-Forwarded-Port:443'],
customResponseHeaders=['X-Client-Geo-Location:US,Mountain View'],
description='My backend service',
healthChecks=[
uri_prefix + 'global/httpHealthChecks/my-health-check-1',
uri_prefix + 'global/httpHealthChecks/my-health-check-2'
],
name='backend-service',
port=80,
portName='http',
protocol=client.messages.BackendService.ProtocolValueValuesEnum.HTTP,
selfLink=uri_prefix + 'global/backendServices/backend-service',
timeoutSec=30,
)
def GetReferenceNormalizers(self, resource_registry):
def MakeReferenceNormalizer(field_name, allowed_collections):
"""Returns a function to normalize resource references."""
def NormalizeReference(reference):
"""Returns normalized URI for field_name."""
try:
value_ref = resource_registry.Parse(reference)
except resources.UnknownCollectionException:
raise InvalidResourceError(
'[{field_name}] must be referenced using URIs.'.format(
field_name=field_name))
if value_ref.Collection() not in allowed_collections:
raise InvalidResourceError(
'Invalid [{field_name}] reference: [{value}].'. format(
field_name=field_name, value=reference))
return value_ref.SelfLink()
return NormalizeReference
# Ensure group is a uri or full collection path representing an instance
# group. Full uris/paths are required because if the user gives us less, we
# don't want to be in the business of guessing health checks.
return [
('healthChecks[]',
MakeReferenceNormalizer(
'healthChecks',
('compute.httpHealthChecks', 'compute.httpsHealthChecks',
'compute.healthChecks', 'compute.regionHealthChecks'))),
('backends[].group',
MakeReferenceNormalizer(
'group',
('compute.instanceGroups', 'compute.regionInstanceGroups'))),
]
def GetGetRequest(self, client, backend_service_ref):
if backend_service_ref.Collection() == 'compute.regionBackendServices':
return (client.apitools_client.regionBackendServices, 'Get',
client.messages.ComputeRegionBackendServicesGetRequest(
**backend_service_ref.AsDict()))
return (client.apitools_client.backendServices, 'Get',
client.messages.ComputeBackendServicesGetRequest(
**backend_service_ref.AsDict()))
def GetSetRequest(self, client, backend_service_ref, replacement):
if backend_service_ref.Collection() == 'compute.regionBackendServices':
return (client.apitools_client.regionBackendServices, 'Update',
client.messages.ComputeRegionBackendServicesUpdateRequest(
backendServiceResource=replacement,
**backend_service_ref.AsDict()))
return (client.apitools_client.backendServices, 'Update',
client.messages.ComputeBackendServicesUpdateRequest(
backendServiceResource=replacement,
**backend_service_ref.AsDict()))