Skip to content

Commit 2538aea

Browse files
authored
Merge pull request #20 from tkdchen/STONEBLD-3422-retain-indentation-style
Retain block sequence indentation
2 parents deaa1a9 + 6fc0c6a commit 2538aea

File tree

6 files changed

+299
-34
lines changed

6 files changed

+299
-34
lines changed

src/pipeline_migration/migrate.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
from pipeline_migration.utils import dump_yaml, load_yaml
1515
from pipeline_migration.registry import Container, Registry, ImageIndex
1616
from pipeline_migration.quay import QuayTagInfo, list_active_repo_tags
17-
from pipeline_migration.utils import is_true
1817
from pipeline_migration.types import FilePath
18+
from pipeline_migration.utils import is_true, YAMLStyle
1919

2020
ANNOTATION_HAS_MIGRATION: Final[str] = "dev.konflux-ci.task.has-migration"
2121
ANNOTATION_IS_MIGRATION: Final[str] = "dev.konflux-ci.task.is-migration"
@@ -105,15 +105,17 @@ def resolve_pipeline(pipeline_file: FilePath) -> Generator[FilePath, Any, None]:
105105
:type pipeline_file: str
106106
:return: a generator yielding a file containing the pipeline definition.
107107
"""
108+
yaml_style = YAMLStyle.detect(pipeline_file)
108109
origin_pipeline = load_yaml(pipeline_file)
110+
109111
if not isinstance(origin_pipeline, dict):
110112
raise ValueError(f"Given file {pipeline_file} is not a YAML mapping.")
111113

112114
kind = origin_pipeline.get("kind")
113115
if kind == TEKTON_KIND_PIPELINE:
114116
yield pipeline_file
115117
pl_yaml = load_yaml(pipeline_file)
116-
dump_yaml(pipeline_file, pl_yaml)
118+
dump_yaml(pipeline_file, pl_yaml, style=yaml_style)
117119
elif kind == TEKTON_KIND_PIPELINE_RUN:
118120
spec = origin_pipeline.get("spec") or {}
119121
if "pipelineSpec" in spec:
@@ -125,7 +127,7 @@ def resolve_pipeline(pipeline_file: FilePath) -> Generator[FilePath, Any, None]:
125127
yield temp_pipeline_file
126128
modified_pipeline = load_yaml(temp_pipeline_file)
127129
spec["pipelineSpec"] = modified_pipeline["spec"]
128-
dump_yaml(pipeline_file, origin_pipeline)
130+
dump_yaml(pipeline_file, origin_pipeline, style=yaml_style)
129131
elif "pipelineRef" in spec:
130132
# Pipeline definition can be referenced here, via either git-resolver or a name field
131133
# pointing to YAML file under the .tekton/.

src/pipeline_migration/utils.py

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,100 @@
1+
from dataclasses import dataclass, field
12
from typing import Any
2-
from ruamel.yaml import YAML
33

44
from pipeline_migration.types import FilePath
55

6+
from ruamel.yaml import YAML, CommentedMap, CommentedSeq
7+
from ruamel.yaml.comments import CommentedBase
8+
9+
10+
__all__ = [
11+
"BlockSequenceIndentation",
12+
"create_yaml_obj",
13+
"dump_yaml",
14+
"is_true",
15+
"load_yaml",
16+
"YAMLStyle",
17+
]
18+
619

720
def is_true(value: str) -> bool:
821
return value.strip().lower() == "true"
922

1023

11-
def create_yaml_obj():
24+
@dataclass
25+
class BlockSequenceIndentation:
26+
indentations: dict[int, int] = field(default_factory=dict)
27+
28+
@property
29+
def is_consistent(self) -> bool:
30+
return len(self.levels) == 1
31+
32+
@property
33+
def levels(self) -> list[int]:
34+
return list(self.indentations.keys())
35+
36+
37+
@dataclass
38+
class _NodePath:
39+
node: CommentedBase
40+
field: str = ""
41+
42+
43+
@dataclass
44+
class YAMLStyle:
45+
indentation: BlockSequenceIndentation
46+
47+
preserve_quotes: bool = True
48+
width: int = 8192
49+
50+
@classmethod
51+
def _detect_block_sequence_indentation(cls, node: CommentedBase) -> BlockSequenceIndentation:
52+
53+
parent_nodes: list[_NodePath] = []
54+
block_seq_indentations = BlockSequenceIndentation()
55+
indentations = block_seq_indentations.indentations
56+
57+
def _walk(node: CommentedBase) -> None:
58+
if isinstance(node, CommentedMap):
59+
parent_nodes.append(_NodePath(node=node))
60+
for key, value in node.items():
61+
parent_nodes[-1].field = key
62+
_walk(value)
63+
parent_nodes.pop()
64+
elif isinstance(node, CommentedSeq):
65+
levels = node.lc.col - parent_nodes[-1].node.lc.col
66+
if levels in indentations:
67+
indentations[levels] += 1
68+
else:
69+
indentations[levels] = 1
70+
for item in node:
71+
_walk(item)
72+
73+
_walk(node)
74+
75+
return block_seq_indentations
76+
77+
@classmethod
78+
def detect(cls, file_path: FilePath) -> "YAMLStyle":
79+
doc = load_yaml(file_path)
80+
indentation = cls._detect_block_sequence_indentation(doc)
81+
return cls(indentation=indentation)
82+
83+
84+
def create_yaml_obj(style: YAMLStyle | None = None):
1285
yaml = YAML()
13-
yaml.preserve_quotes = True
14-
yaml.width = 8192
86+
if style is None:
87+
return yaml
88+
89+
yaml.preserve_quotes = style.preserve_quotes
90+
yaml.width = style.width
91+
92+
offset = 0
93+
if style.indentation.is_consistent:
94+
offset = style.indentation.levels[0]
95+
sequence = offset + 2
96+
yaml.indent(sequence=sequence, offset=offset)
97+
1598
return yaml
1699

17100

@@ -20,6 +103,6 @@ def load_yaml(yaml_file: FilePath) -> Any:
20103
return create_yaml_obj().load(f)
21104

22105

23-
def dump_yaml(yaml_file: FilePath, data: Any):
106+
def dump_yaml(yaml_file: FilePath, data: Any, style: YAMLStyle | None = None) -> None:
24107
with open(yaml_file, "w", encoding="utf-8") as f:
25-
create_yaml_obj().dump(data, f)
108+
create_yaml_obj(style).dump(data, f)

tests/conftest.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
from copy import deepcopy
3+
from textwrap import dedent
34
from typing import Final
45

56
import responses
@@ -107,3 +108,43 @@ def _mock(c: Container, has_migration=False, previous_migration_bundle: str | No
107108
responses.add(responses.GET, f"https://{c.manifest_url()}", json=manifest_json)
108109

109110
return _mock
111+
112+
113+
@pytest.fixture
114+
def pipeline_yaml():
115+
return dedent(
116+
"""\
117+
apiVersion: tekton.dev/v1
118+
kind: Pipeline
119+
metadata:
120+
name: pl
121+
spec:
122+
tasks:
123+
- name: clone
124+
image: debian:latest
125+
script: |
126+
git clone https://git.host/project
127+
"""
128+
)
129+
130+
131+
@pytest.fixture(params=["no_indents", "2_spaces_indents"])
132+
def pipeline_yaml_with_various_indent_styles(request, pipeline_yaml):
133+
match request.param:
134+
case "no_indents":
135+
return pipeline_yaml
136+
case "2_spaces_indents":
137+
return dedent(
138+
"""\
139+
apiVersion: tekton.dev/v1
140+
kind: Pipeline
141+
metadata:
142+
name: pl
143+
spec:
144+
tasks:
145+
- name: clone
146+
image: debian:latest
147+
script: |
148+
git clone https://git.host/project
149+
"""
150+
)

tests/test_cli.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
ensure_container,
2424
)
2525

26-
from tests.test_migrate import APP_IMAGE_REPO, PIPELINE_DEFINITION, TASK_BUNDLE_CLONE
26+
from pipeline_migration.utils import YAMLStyle
27+
from tests.test_migrate import APP_IMAGE_REPO, TASK_BUNDLE_CLONE
2728
from tests.utils import generate_digest, generate_git_sha
2829

2930

@@ -218,20 +219,29 @@ def _mock_quay_list_tags(self):
218219
json={"tags": tags, "page": 1, "has_additional": False},
219220
)
220221

221-
def _mock_pipeline_file(self, tmp_path: Path) -> Path:
222+
def _mock_pipeline_file(self, tmp_path: Path, content: str) -> Path:
222223
tekton_dir = tmp_path / ".tekton"
223224
tekton_dir.mkdir()
224225
pipeline_file = tekton_dir / "component-pipeline.yaml"
225-
pipeline_file.write_text(PIPELINE_DEFINITION)
226+
pipeline_file.write_text(content)
226227
return pipeline_file
227228

228229
@responses.activate
229230
@pytest.mark.parametrize("use_linked_migrations", [True, False])
230-
def test_apply_migrations(self, use_linked_migrations, monkeypatch, tmp_path):
231+
def test_apply_migrations(
232+
self,
233+
use_linked_migrations,
234+
pipeline_yaml_with_various_indent_styles,
235+
monkeypatch,
236+
tmp_path,
237+
):
231238
monkeypatch.setattr("pipeline_migration.migrate.Registry", MockRegistry)
232239
self._mock_quay_list_tags()
233240

234-
pipeline_file = self._mock_pipeline_file(tmp_path)
241+
pipeline_file = self._mock_pipeline_file(tmp_path, pipeline_yaml_with_various_indent_styles)
242+
243+
# Verified later
244+
origin_style = YAMLStyle.detect(pipeline_file)
235245

236246
tb_upgrades = [
237247
{
@@ -292,6 +302,14 @@ def _subprocess_run(cmd, *args, **kwargs):
292302

293303
assert entry_point() is None
294304

305+
# Verify result formatting
306+
cur_style = YAMLStyle.detect(pipeline_file)
307+
assert cur_style.indentation.is_consistent
308+
if origin_style.indentation.is_consistent:
309+
assert origin_style.indentation.levels == cur_style.indentation.levels
310+
else:
311+
assert cur_style.indentation.levels == [0]
312+
295313

296314
def test_entry_point_should_catch_error(monkeypatch, caplog):
297315
cli_cmd = ["pmt", "--use-legacy-resolver", "-u", json.dumps(UPGRADES)]

tests/test_migrate.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -646,26 +646,10 @@ def _fetch_migration_file(image: str, digest: str) -> str | None:
646646
assert script_content == migrations[0].migration_script
647647

648648

649-
PIPELINE_DEFINITION: Final = dedent(
650-
"""\
651-
apiVersion: tekton.dev/v1
652-
kind: Pipeline
653-
metadata:
654-
name: pl
655-
spec:
656-
tasks:
657-
- name: clone
658-
image: debian:latest
659-
script: |
660-
git clone https://git.host/project
661-
"""
662-
)
663-
664-
665649
class TestApplyMigrations:
666650

667651
@pytest.mark.parametrize("chdir", [True, False])
668-
def test_apply_migrations(self, chdir, tmp_path, monkeypatch):
652+
def test_apply_migrations(self, pipeline_yaml, chdir, tmp_path, monkeypatch):
669653
"""Ensure applying all resolved migrations"""
670654

671655
renovate_upgrades = [
@@ -703,7 +687,7 @@ def test_apply_migrations(self, chdir, tmp_path, monkeypatch):
703687
tekton_dir = tmp_path / ".tekton"
704688
tekton_dir.mkdir()
705689
package_file = tekton_dir / "pipeline.yaml"
706-
package_file.write_text(PIPELINE_DEFINITION)
690+
package_file.write_text(pipeline_yaml)
707691

708692
test_context = {"executed_scripts": []}
709693
counter = itertools.count()
@@ -733,7 +717,9 @@ def subprocess_run(*args, **kwargs):
733717
with pytest.raises(ValueError, match="Pipeline file does not exist: .+"):
734718
manager.apply_migrations()
735719

736-
def test_raise_error_if_migration_process_fails(self, caplog, monkeypatch, tmp_path):
720+
def test_raise_error_if_migration_process_fails(
721+
self, pipeline_yaml, caplog, monkeypatch, tmp_path
722+
):
737723
caplog.set_level(logging.DEBUG, logger="migrate")
738724

739725
renovate_upgrades = [
@@ -771,7 +757,7 @@ def test_raise_error_if_migration_process_fails(self, caplog, monkeypatch, tmp_p
771757
tekton_dir = tmp_path / ".tekton"
772758
tekton_dir.mkdir()
773759
package_file = tekton_dir / "pipeline.yaml"
774-
package_file.write_text(PIPELINE_DEFINITION)
760+
package_file.write_text(pipeline_yaml)
775761

776762
def _mkstemp(*args, **kwargs):
777763
tmp_file_path = tmp_path / "migration_file"

0 commit comments

Comments
 (0)