Skip to content

Commit 3593ff5

Browse files
hubatishcopybara-github
authored andcommitted
Move yaml code from container_service -> vm_util
PiperOrigin-RevId: 846416972
1 parent 26a73cf commit 3593ff5

File tree

6 files changed

+181
-103
lines changed

6 files changed

+181
-103
lines changed

perfkitbenchmarker/container_service/kubernetes.py

Lines changed: 6 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
"""Contains classes related to managed kubernetes container services."""
1515

1616
import calendar
17-
import collections
1817
import dataclasses
1918
import datetime
2019
import functools
@@ -487,18 +486,6 @@ def _WriteAndApplyManifest(
487486
)
488487
return resource_names
489488

490-
@staticmethod
491-
def _RecursiveDict() -> dict[str, Any]:
492-
"""Creates a new dictionary with auto nesting keys.
493-
494-
See https://stackoverflow.com/a/10218517/2528472.
495-
496-
Returns:
497-
A new dictionary that automatically sets the value for any nested missing
498-
keys.
499-
"""
500-
return collections.defaultdict(KubernetesClusterCommands._RecursiveDict)
501-
502489
@staticmethod
503490
def ConvertManifestToYamlDicts(
504491
manifest_file: str, **kwargs
@@ -515,87 +502,24 @@ def ConvertManifestToYamlDicts(
515502
manifest = vm_util.ReadAndRenderJinja2Template(
516503
manifest_file, trim_spaces=False, **kwargs
517504
)
518-
yaml_docs = yaml.safe_load_all(manifest)
519-
return_yamls = []
520-
for yaml_doc in yaml_docs:
521-
return_yamls.append(
522-
KubernetesClusterCommands._ConvertToDictType(
523-
yaml_doc, KubernetesClusterCommands._RecursiveDict
524-
)
525-
)
526-
return return_yamls
527-
528-
@staticmethod
529-
def _ConvertToDictType(
530-
yaml_doc: Any, dict_lambda: Any
531-
) -> dict[str, Any] | Any:
532-
"""Converts a YAML document to the given dictionary type.
533-
534-
In particular a recursive defaultdict can be more easily accessed with e.g.
535-
my_dict['a']['b'] = value rather than my_dict.setdefault('a', {})['b'] =
536-
value.
537-
538-
Args:
539-
yaml_doc: The YAML document to convert.
540-
dict_lambda: A constructor for the dictionary type to convert to.
541-
542-
Returns:
543-
The remade dictionary.
544-
"""
545-
if not isinstance(yaml_doc, dict) and not isinstance(yaml_doc, list):
546-
return yaml_doc
547-
548-
def _ConvertPossiblyEmptyValue(value: Any) -> Any | None:
549-
"""Converts a given value to the dictionary type, or None if empty."""
550-
if not bool(value) and value != 0:
551-
return None
552-
converted_value = KubernetesClusterCommands._ConvertToDictType(
553-
value, dict_lambda
554-
)
555-
if not bool(converted_value) and converted_value != 0:
556-
return None
557-
return converted_value
558-
559-
yaml_list = []
560-
if isinstance(yaml_doc, list):
561-
for item in yaml_doc:
562-
converted_value = _ConvertPossiblyEmptyValue(item)
563-
if converted_value is None:
564-
continue
565-
yaml_list.append(converted_value)
566-
return yaml_list
567-
yaml_dict = dict_lambda()
568-
for key, value in yaml_doc.items():
569-
converted_value = _ConvertPossiblyEmptyValue(value)
570-
if converted_value is None:
571-
continue
572-
yaml_dict[key] = converted_value
573-
return yaml_dict
505+
return vm_util.ReadYamlAsDicts(manifest)
574506

575507
@staticmethod
576508
def ApplyYaml(
577-
yaml_dicts: list[dict[str, Any]], should_log_file: bool = True
509+
yaml_dicts: list[dict[str, Any]], **logging_kwargs
578510
) -> Iterator[str]:
579511
"""Writes yaml to a file and applies it.
580512
581513
Args:
582514
yaml_dicts: A list of YAML documents.
583-
should_log_file: Whether to log the rendered manifest to stdout or not.
515+
**logging_kwargs: Keyword arguments passed to file writing.
584516
585517
Returns:
586518
Names of the resources, e.g. [deployment.apps/mydeploy, pod/foo]
587519
"""
588-
normal_dicts = []
589-
# Convert back to a normal dict because yaml.dump otherwise adds random
590-
# "dictitems:" keys & other python artifacts.
591-
for yaml_dict in yaml_dicts:
592-
normal_dicts.append(
593-
KubernetesClusterCommands._ConvertToDictType(yaml_dict, dict)
594-
)
595-
manifest = yaml.dump_all(normal_dicts)
596-
return KubernetesClusterCommands._WriteAndApplyManifest(
597-
manifest, should_log_file
598-
)
520+
vm_util.IncrementStackLevel(**logging_kwargs)
521+
yaml_file = vm_util.WriteYaml(yaml_dicts, **logging_kwargs)
522+
return KubernetesClusterCommands._ApplyRenderedManifest(yaml_file)
599523

600524
@staticmethod
601525
def WaitForResource(

perfkitbenchmarker/os_mixin.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ def PushDataFile(self, data_file, remote_path='', should_double_copy=None):
570570
self.PushFile(file_path, remote_path)
571571

572572
def RenderTemplate(
573-
self, template_path, remote_path, context, should_log_file: bool = False
573+
self, template_path, remote_path, context, **logging_kwargs
574574
):
575575
"""Renders a local Jinja2 template and copies it to the remote host.
576576
@@ -581,16 +581,19 @@ def RenderTemplate(
581581
template_path: string. Local path to jinja2 template.
582582
remote_path: string. Remote path for rendered file on the remote vm.
583583
context: dict. Variables to pass to the Jinja2 template during rendering.
584-
should_log_file: bool. Whether to log the file after rendering.
584+
**logging_kwargs: dict. Keyword arguments passed to WriteTemporaryFile.
585585
586586
Raises:
587587
jinja2.UndefinedError: if template contains variables not present in
588588
'context'.
589589
RemoteCommandError: If there was a problem copying the file.
590590
"""
591591
context = {'vm': self} | context
592+
vm_util.IncrementStackLevel(**logging_kwargs)
593+
if 'should_log_file' not in logging_kwargs:
594+
logging_kwargs['should_log_file'] = False
592595
rendered_template = vm_util.RenderTemplate(
593-
template_path, context, should_log_file
596+
template_path, context, **logging_kwargs
594597
)
595598
self.RemoteCopy(rendered_template, remote_path)
596599

perfkitbenchmarker/vm_util.py

Lines changed: 144 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Set of utility functions for working with virtual machines."""
1616

1717

18+
import collections
1819
import contextlib
1920
import enum
2021
import logging
@@ -36,6 +37,7 @@
3637
from perfkitbenchmarker import errors
3738
from perfkitbenchmarker import log_util
3839
from perfkitbenchmarker import temp_dir
40+
import yaml
3941

4042
FLAGS = flags.FLAGS
4143
# Using logger rather than logging.info to avoid stack_level problems.
@@ -925,11 +927,48 @@ def DictionaryToEnvString(dictionary, joiner=' '):
925927
)
926928

927929

930+
def WriteTemporaryFile(
931+
file_contents: str,
932+
origin: str = '',
933+
should_log_file: bool = True,
934+
stack_level: int = 1,
935+
) -> str:
936+
"""Writes the string file_contents to a temporary file.
937+
938+
Args:
939+
file_contents: string. The contents of the file to write.
940+
origin: str. The origin of the file, used for logging.
941+
should_log_file: bool. Whether to log the file before writing.
942+
stack_level: int. The stack level to use for logging.
943+
944+
Returns:
945+
The name of the temporary file.
946+
"""
947+
stack_level += 1
948+
prefix = 'pkb-'
949+
with NamedTemporaryFile(
950+
prefix=prefix, dir=GetTempDir(), delete=False, mode='w'
951+
) as tf:
952+
if origin:
953+
origin = f'from {origin}'
954+
if should_log_file:
955+
logging.info(
956+
'Writing temporary file %s to name %s with contents:\n%s',
957+
origin,
958+
tf.name,
959+
file_contents,
960+
stacklevel=stack_level,
961+
)
962+
tf.write(file_contents)
963+
tf.close()
964+
return tf.name
965+
966+
928967
def RenderTemplate(
929968
template_path,
930969
context,
931-
should_log_file: bool = False,
932970
trim_spaces: bool = False,
971+
**logging_kwargs: dict[str, Any],
933972
) -> str:
934973
"""Renders a local Jinja2 template and returns its file name.
935974
@@ -938,8 +977,8 @@ def RenderTemplate(
938977
Args:
939978
template_path: string. Local path to jinja2 template.
940979
context: dict. Variables to pass to the Jinja2 template during rendering.
941-
should_log_file: bool. Whether to log the file after rendering.
942980
trim_spaces: bool. Value for both trim_blocks and lstrip_blocks.
981+
**logging_kwargs: dict. Keyword arguments passed to WriteTemporaryFile.
943982
944983
Raises:
945984
jinja2.UndefinedError: if template contains variables not present in
@@ -951,24 +990,14 @@ def RenderTemplate(
951990
rendered_template = ReadAndRenderJinja2Template(
952991
template_path, trim_spaces, **context
953992
)
954-
prefix = 'pkb-' + os.path.basename(template_path)
955-
with NamedTemporaryFile(
956-
prefix=prefix, dir=GetTempDir(), delete=False, mode='w'
957-
) as tf:
958-
if should_log_file:
959-
logging.info(
960-
'Rendered template from %s to %s with full text:\n%s',
961-
template_path,
962-
tf.name,
963-
rendered_template,
964-
stacklevel=2,
965-
)
966-
tf.write(rendered_template)
967-
tf.close()
968-
return tf.name
993+
IncrementStackLevel(**logging_kwargs)
994+
return WriteTemporaryFile(
995+
rendered_template,
996+
origin=f'jinja2 template {template_path}',
997+
**logging_kwargs,
998+
)
969999

9701000

971-
@staticmethod
9721001
def ReadAndRenderJinja2Template(
9731002
file_path: str, trim_spaces: bool = False, **kwargs
9741003
) -> str:
@@ -1003,3 +1032,100 @@ def ReadLocalFile(filename: str) -> str:
10031032
file_path = posixpath.join(GetTempDir(), filename)
10041033
stdout, _, _ = IssueCommand(['cat', file_path])
10051034
return stdout
1035+
1036+
1037+
def RecursiveDict() -> dict[str, Any]:
1038+
"""Creates a new dictionary with auto nesting keys.
1039+
1040+
See https://stackoverflow.com/a/10218517/2528472.
1041+
1042+
Returns:
1043+
A new dictionary that automatically sets the value for any nested missing
1044+
keys.
1045+
"""
1046+
return collections.defaultdict(RecursiveDict)
1047+
1048+
1049+
def ReadYamlAsDicts(
1050+
file_contents: str
1051+
) -> list[dict[str, Any]]:
1052+
"""Reads file contents & converts it to a list of recursive YAML doc dicts.
1053+
1054+
Args:
1055+
file_contents: The yaml string.
1056+
1057+
Returns:
1058+
The various YAML documents as a list of recursive dictionaries.
1059+
"""
1060+
yaml_docs = yaml.safe_load_all(file_contents)
1061+
return_yamls = []
1062+
for yaml_doc in yaml_docs:
1063+
return_yamls.append(ConvertToDictType(yaml_doc, RecursiveDict))
1064+
return return_yamls
1065+
1066+
1067+
def ConvertToDictType(yaml_doc: Any, dict_lambda: Any) -> dict[str, Any] | Any:
1068+
"""Converts a YAML document to the given dictionary type.
1069+
1070+
In particular a recursive defaultdict can be more easily accessed with e.g.
1071+
my_dict['a']['b'] = value rather than my_dict.setdefault('a', {})['b'] =
1072+
value.
1073+
1074+
Args:
1075+
yaml_doc: The YAML document to convert.
1076+
dict_lambda: A constructor for the dictionary type to convert to.
1077+
1078+
Returns:
1079+
The remade dictionary.
1080+
"""
1081+
if not isinstance(yaml_doc, dict) and not isinstance(yaml_doc, list):
1082+
return yaml_doc
1083+
1084+
def _ConvertPossiblyEmptyValue(value: Any) -> Any | None:
1085+
"""Converts a given value to the dictionary type, or None if empty."""
1086+
if not bool(value) and value != 0:
1087+
return None
1088+
converted_value = ConvertToDictType(value, dict_lambda)
1089+
if not bool(converted_value) and converted_value != 0:
1090+
return None
1091+
return converted_value
1092+
1093+
yaml_list = []
1094+
if isinstance(yaml_doc, list):
1095+
for item in yaml_doc:
1096+
converted_value = _ConvertPossiblyEmptyValue(item)
1097+
if converted_value is None:
1098+
continue
1099+
yaml_list.append(converted_value)
1100+
return yaml_list
1101+
yaml_dict = dict_lambda()
1102+
for key, value in yaml_doc.items():
1103+
converted_value = _ConvertPossiblyEmptyValue(value)
1104+
if converted_value is None:
1105+
continue
1106+
yaml_dict[key] = converted_value
1107+
return yaml_dict
1108+
1109+
1110+
def WriteYaml(
1111+
yaml_dicts: list[dict[str, Any]], **kwargs
1112+
) -> str:
1113+
"""Writes yaml to a file & returns the name of that file.
1114+
1115+
Args:
1116+
yaml_dicts: A list of YAML documents.
1117+
**kwargs: Keyword arguments passed to file writing.
1118+
1119+
Returns:
1120+
Names of the written file.
1121+
"""
1122+
normal_dicts = []
1123+
# Convert back to a normal dict because yaml.dump otherwise adds random
1124+
# "dictitems:" keys & other python artifacts.
1125+
for yaml_dict in yaml_dicts:
1126+
normal_dicts.append(ConvertToDictType(yaml_dict, dict))
1127+
manifest = yaml.dump_all(normal_dicts)
1128+
IncrementStackLevel(**kwargs)
1129+
return WriteTemporaryFile(
1130+
manifest, origin='yaml', **kwargs
1131+
)

tests/container_service_test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import tempfile
23
import time
34
from typing import Callable, Iterable, Protocol, Tuple
45
import unittest
@@ -163,6 +164,13 @@ def test_apply_manifest_yaml_logs(self):
163164
),
164165
)
165166
)
167+
self.enter_context(
168+
mock.patch.object(
169+
vm_util,
170+
'GetTempDir',
171+
return_value=tempfile.gettempdir(),
172+
)
173+
)
166174
with self.assertLogs(level='INFO') as logs:
167175
yamls = self.kubernetes_cluster.ConvertManifestToYamlDicts(
168176
'tests/data/kube_apply.yaml.j2',

0 commit comments

Comments
 (0)