File: //snap/google-cloud-cli/current/lib/googlecloudsdk/api_lib/accesscontextmanager/acm_printer.py
# -*- coding: utf-8 -*- #
# 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.
"""Unified diff resource printer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import difflib
import io
import re
from googlecloudsdk.core import exceptions
from googlecloudsdk.core.resource import resource_printer_base
from googlecloudsdk.core.resource import resource_projection_spec
from googlecloudsdk.core.resource import resource_projector
from googlecloudsdk.core.resource import resource_transform
from googlecloudsdk.core.resource import yaml_printer
class ACMDiffPrinter(resource_printer_base.ResourcePrinter):
"""A printer for an ndiff of the first two projection columns.
A unified diff of the first two projection columns.
Printer attributes:
format: The format of the diffed resources. Each resource is converted
to this format and the diff of the converted resources is displayed.
The default is 'yaml'.
"""
def __init__(self, *args, **kwargs):
super(ACMDiffPrinter, self).__init__(
*args, by_columns=True, non_empty_projection_required=True, **kwargs)
self._print_format = self.attributes.get('format', 'yaml')
def _Diff(self, old, new):
"""Prints a modified ndiff of formatter output for old and new.
IngressPolicies:
ingressFrom:
sources:
accessLevel: accessPolicies/123456789/accessLevels/my_level
-resource: projects/123456789012
+resource: projects/234567890123
EgressPolicies:
+egressTo:
+operations:
+actions:
+action: method_for_all
+actionType: METHOD
+serviceName: chemisttest.googleapis.com
+resources:
+projects/345678901234
Args:
old: The old original resource.
new: The new changed resource.
"""
# Fill a buffer with the object as rendered originally.
buf_old = io.StringIO()
printer = self.Printer(self._print_format, out=buf_old)
printer.PrintSingleRecord(old)
# Fill a buffer with the object as rendered after the change.
buf_new = io.StringIO()
printer = self.Printer(self._print_format, out=buf_new)
printer.PrintSingleRecord(new)
lines_old = ''
lines_new = ''
# Send these two buffers to the ndiff() function for printing.
if old is not None:
lines_old = self._FormatYamlPrinterLinesForDryRunDescribe(
buf_old.getvalue().split('\n'))
if new is not None:
lines_new = self._FormatYamlPrinterLinesForDryRunDescribe(
buf_new.getvalue().split('\n'))
lines_diff = difflib.ndiff(lines_old, lines_new)
empty_line_pattern = re.compile(r'^\s*$')
empty_config_pattern = re.compile(r'^(\+|-)\s+\{\}$')
for line in lines_diff:
# We want to show the entire contents of resource, but without the
# additional information added by ndiff, which always leads with '?'. We
# also don't want to show empty lines produced from comparing unset
# fields, as well as lines produced from comparing empty messages, which
# will look like '+ {}' or '- {}'.
if line and line[0] != '?' and not empty_line_pattern.match(
line) and not empty_config_pattern.match(line):
print(line)
def _AddRecord(self, record, delimit=False):
"""Immediately prints the first two columns of record as a unified diff.
Records with less than 2 columns are silently ignored.
Args:
record: A JSON-serializable object.
delimit: Prints resource delimiters if True.
"""
title = self.attributes.get('title')
if title:
self._out.Print(title)
self._title = None
if len(record) > 1:
self._Diff(record[0], record[1])
def _FormatYamlPrinterLinesForDryRunDescribe(self, lines):
"""Tweak yaml printer formatted resources for ACM's dry run describe output.
Args:
lines: yaml printer formatted strings
Returns:
lines with no '-' prefix for yaml array elements.
"""
return [line.replace('-', ' ', 1) for line in lines]
class Error(exceptions.Error):
"""Exceptions for this module."""
class UnknownFormatError(Error):
"""Unknown format name exception."""
_FORMATTERS = {
'default': yaml_printer.YamlPrinter,
'diff': ACMDiffPrinter,
'yaml': yaml_printer.YamlPrinter,
}
def Print(resources, print_format, out=None, defaults=None, single=False):
"""Prints the given resources.
Args:
resources: A singleton or list of JSON-serializable Python objects.
print_format: The _FORMATTER name with optional projection expression.
out: Output stream, log.out if None.
defaults: Optional resource_projection_spec.ProjectionSpec defaults.
single: If True then resources is a single item and not a list. For example,
use this to print a single object as JSON.
"""
printer = Printer(print_format, out=out, defaults=defaults)
# None means the printer is disabled.
if printer:
printer.Print(resources, single)
def Printer(print_format, out=None, defaults=None, console_attr=None):
"""Returns a resource printer given a format string.
Args:
print_format: The _FORMATTERS name with optional attributes and projection.
out: Output stream, log.out if None.
defaults: Optional resource_projection_spec.ProjectionSpec defaults.
console_attr: The console attributes for the output stream. Ignored by some
printers. If None then printers that require it will initialize it to
match out.
Raises:
UnknownFormatError: The print_format is invalid.
Returns:
An initialized ResourcePrinter class or None if printing is disabled.
"""
projector = resource_projector.Compile(
expression=print_format,
defaults=resource_projection_spec.ProjectionSpec(
defaults=defaults, symbols=resource_transform.GetTransforms()))
printer_name = projector.Projection().Name()
if not printer_name:
# Do not print, do not consume resources.
return None
try:
printer_class = _FORMATTERS[printer_name]
except KeyError:
raise UnknownFormatError("""\
Format for acm_printer must be one of {0}; received [{1}].
""".format(', '.join(SupportedFormats()), printer_name))
printer = printer_class(
out=out,
name=printer_name,
printer=Printer,
projector=projector,
console_attr=console_attr)
return printer
def SupportedFormats():
"""Returns a sorted list of supported format names."""
return sorted(_FORMATTERS)