debian-forge/osbuild/formats/v1.py
Christian Kellner 569345cc72 pipeline: identify pipelines by name
Every pipeline that gets added to the `Manifest` now need to have
a unique name by which it can be identified. The version 1 format
loader is changed so that the main pipeline that builds the tree
is always called `tree`. The build pipeline for it will be called
`build` and further recursive build pipelines `build-build`, where
the number of repetitions of `build` corresponds to their level of
nesting. An assembler, if it exists, will be added as `assembler`.
The `Manifest.__getitem__` helper is changed so it will first try
to access pipeline via its name and then fall back to an id based
search. NB: in the degenrate case of multiple pipelines that have
exactly the same `id`, i.e. same stages, with the same options and
same build pipeline, only the first one will be return; but only
the first one here will be built as well, so this is in practice
not a problem.
The formatter uses this helper to get the tree pipeline  via its
name wherever it is needed.
This also adds an `__iter__` method `Manifest` to ease iterating
over just the pipeline values, a la `for pipeline in manifet`.
2021-01-22 15:03:19 +01:00

190 lines
6.2 KiB
Python

# Version 1 of the manifest description
from typing import Dict, Optional, Tuple
from osbuild.meta import Index, ValidationResult
from ..inputs import Input
from ..pipeline import Manifest, Pipeline, detect_host_runner
def describe(manifest: Manifest, *, with_id=False) -> Dict:
"""Create the manifest description for the pipeline"""
def describe_stage(stage):
description = {"name": stage.name}
if stage.options:
description["options"] = stage.options
if with_id:
description["id"] = stage.id
return description
def describe_pipeline(pipeline: Pipeline) -> Dict:
description = {}
if pipeline.build:
build = manifest[pipeline.build]
description["build"] = {
"pipeline": describe_pipeline(build),
"runner": pipeline.runner
}
if pipeline.stages:
stages = [describe_stage(s) for s in pipeline.stages]
description["stages"] = stages
if pipeline.assembler:
assembler = describe_stage(pipeline.assembler)
description["assembler"] = assembler
return description
description = {
"pipeline": describe_pipeline(manifest["tree"])
}
if manifest.sources:
description["sources"] = manifest.sources
return description
def load_build(description: Dict, index: Index, manifest: Manifest, n: int):
pipeline = description.get("pipeline")
if pipeline:
build_pipeline = load_pipeline(pipeline, index, manifest, n + 1)
else:
build_pipeline = None
return build_pipeline, description["runner"]
def load_pipeline(description: Dict, index: Index, manifest: Manifest, n: int = 0) -> Pipeline:
build = description.get("build")
if build:
build_pipeline, runner = load_build(build, index, manifest, n)
else:
build_pipeline, runner = None, detect_host_runner()
# the "main" pipeline is called `tree`, since it is building the
# tree that will later be used by the `assembler`. Nested build
# pipelines will get call "build", and "build-build-...", where
# the number of repetitions is equal their level of nesting
if not n:
name = "tree"
else:
name = "-".join(["build"] * n)
build_id = build_pipeline and build_pipeline.tree_id
pipeline = manifest.add_pipeline(name, runner, build_id)
for s in description.get("stages", []):
info = index.get_module_info("Stage", s["name"])
pipeline.add_stage(info, s.get("options", {}))
a = description.get("assembler")
if a:
info = index.get_module_info("Assembler", a["name"])
asm = pipeline.set_assembler(info, a.get("options", {}))
info = index.get_module_info("Input", "org.osbuild.tree")
asm.inputs = {
"tree": Input(info, {"pipeline": {"id": pipeline.tree_id}})
}
pipeline.export = True
return pipeline
def load(description: Dict, index: Index) -> Manifest:
"""Load a manifest description"""
pipeline = description.get("pipeline", {})
sources = description.get("sources", {})
manifest = Manifest(sources)
load_pipeline(pipeline, index, manifest)
for pipeline in manifest.pipelines.values():
for stage in pipeline.stages:
stage.sources = sources
return manifest
def get_ids(manifest: Manifest) -> Tuple[Optional[str], Optional[str]]:
pipeline = manifest["tree"]
return pipeline.tree_id, pipeline.output_id
def output(manifest: Manifest, res: Dict) -> Dict:
"""Convert a result into the v1 format"""
def result_for_pipeline(pipeline):
# The pipeline might not have been built one of its
# dependencies, i.e. its build pipeline, failed to
# build. We thus need to be tolerant of a missing
# result but still need to to recurse
current = res.get(pipeline.id, {})
retval = {
"success": current.get("success", False)
}
if pipeline.build:
build = manifest[pipeline.build]
retval["build"] = result_for_pipeline(build)
stages = current.get("stages")
if stages:
retval["stages"] = stages
assembler = current.get("assembler")
if assembler:
retval["assembler"] = assembler
return retval
return result_for_pipeline(manifest["tree"])
def validate(manifest: Dict, index: Index) -> ValidationResult:
"""Validate a OSBuild manifest
This function will validate a OSBuild manifest, including
all its stages and assembler and build manifests. It will
try to validate as much as possible and not stop on errors.
The result is a `ValidationResult` object that can be used
to check the overall validation status and iterate all the
individual validation errors.
"""
schema = index.get_schema("Manifest")
result = schema.validate(manifest)
# main pipeline
pipeline = manifest.get("pipeline", {})
# recursively validate the build pipeline as a "normal"
# pipeline in order to validate its stages and assembler
# options; for this it is being re-parented in a new plain
# {"pipeline": ...} dictionary. NB: Any nested structural
# errors might be detected twice, but de-duplicated by the
# `ValidationResult.merge` call
build = pipeline.get("build", {}).get("pipeline")
if build:
res = validate({"pipeline": build}, index=index)
result.merge(res, path=["pipeline", "build"])
stages = pipeline.get("stages", [])
for i, stage in enumerate(stages):
name = stage["name"]
schema = index.get_schema("Stage", name)
res = schema.validate(stage)
result.merge(res, path=["pipeline", "stages", i])
asm = pipeline.get("assembler", {})
if asm:
name = asm["name"]
schema = index.get_schema("Assembler", name)
res = schema.validate(asm)
result.merge(res, path=["pipeline", "assembler"])
# sources
sources = manifest.get("sources", {})
for name, source in sources.items():
schema = index.get_schema("Source", name)
res = schema.validate(source)
result.merge(res, path=["sources", name])
return result