207 lines
6.9 KiB
Python
207 lines
6.9 KiB
Python
# -*- 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)
|