This document explains how manifests are generated in code. It is useful for understanding how (and where) changes should be made in the pipeline generation code to have the desired effect.
Manifests are generated in two general stages: Instantiation and Serialization.
- Instantiation: Creates an object that implements the
manifest.Manifest
interface.- Creating this requires a number of steps. See Manifest Instantiation.
- An instantiated
Manifest
contains:- Source specifications for content: package names, containers, ostree commits.
- An array of
manifest.Pipeline
objects, each with necessary customizations and serialization methods for producing stage-sequences.
- Serialization: Creates the sequence of stages based on each pipeline and
produces
manifest.OSBuildManifest
, which is a[]byte
array with custom unmarshalling/marshalling methods.- This stage requires the content specifications resolved from the manifest source specifications (package specs, container specs, ostree commit specs). See Resolving Content.
The makeManifest()
function in cmd/build/main.go
is a straightforward
implementation of the sequence of actions required to generate a manifest
described below.
Instantiating a manifest involves generating an array of Pipelines that will produce an image. Each pipeline supports different options that will affect the stages and stage options it will generate.
Typically, manifest instantiation happens inside the
ImageType.Manifest()
function, which each distro
implements separately. The function is responsible for:
- Validating blueprint customizations for the selected image type.
- Collecting static package sets for the distro and image type.
- Collecting container source specs from the blueprint customizations.
- Calling the image function for the image type, which creates an
image.ImageKind
object, and - Instantiating the manifest using
ImageKind.InstantiateManifest()
.
The OS pipeline is the biggest and most central pipeline. It is responsible for generating the stages that will create the main OS tree of the resulting image. Almost all customizations that control specific options for a bootable image are defined on this pipeline.
The Build pipeline is used in every manifest to define a build root for all the following pipelines to run in. It is always added first and is almost never customized beyond package selection.
The OS pipeline's private getPackageSetChains()
method (which is called by
Manifest.GetPackageSetChains()
)
is a good example of dynamic package selection based on enabled features and
customizations. Packages are selected based on features that will be required
on the running system. For example, if NTP servers are defined for the image,
the chrony
package is selected.
The build pipeline is special in that extra packages are added to it
dynamically based on the requirements of subsequent pipelines. Pipelines can
define build packages that they require in the getBuildPackages()
private
method. These packages will be merged with the static packages that are defined
for the build root. For example, if a container will be embedded in the OS
pipeline, the skopeo
package is added to the build root to copy the container
into the OS container store.
Manifest
objects should provide source
specifications for the content they need. Each source specification should be
resolved to a content specification and passed to the serialization function to
create the final manifest
All source and content specifications have type map[string][]<spec>
(where
<spec>
is the base type for the source or content specification). The map key
is the name of the pipeline that requires the content and multiple specs can be
assigned to each pipeline.
Currently there are three methods for three types of content that needs to be resolved.
Source specification: The source specification for packages is the
rpmmd.PackageSet
. Each package set defines a list of
package names to include, a list to exclude, and a set of RPM repositories to
use to resolve and retrieve the packages.
Note: The package source specification is special in that it defines an array
of package sets, which means each element in the array specifies multiple
packages. We sometimes refer to this array as a package set chain. Chains of
package sets are depsolved in order as part of the same call to dnf-json
. The
result of a depsolve of each package set in the chain is merged with the
subsequent set and the result is a single array of package specs.
Content specification: The content specification for packages is the
rpmmd.PackageSpec
. Each package spec is a fully resolved
description of an RPM, with metadata, a checksum, and a URL from which to
retrieve the package.
Resolving: Resolving package sets to package specs is done using
the dnfjson.Solver.Depsolve()
function. This
call resolves the dependencies of an array of package sets and returns all the
packages that were specified, their dependencies, and the metadata for each
package.
Source specification: The source specification for containers is the
container.SourceSpec
. The main component is the
Source
, which is a full ref to a container in a registry.
Content specification: The content specification for containers is the
container.Spec
. Each container spec is a fully
resolved description of a container.
Resolving: Resolving container source specs to container specs is
done using the container.Resolver
type. Container
source specs are added to the resolver with the
Resolver.Add()
method and all results are
retrieved with the Resolver.Finish()
method.
Source specification: The source specification for ostree commits is the
ostree.SourceSpec
. It contains a URL to an ostree
repository and a ref to resolve.
Content specification: The content specification for ostree commits is the
ostree.CommitSpec
. Each ostree commit spec
contains the URL and ref from the source spec as well as the checksum of the
commit.
Resolving: Resolving ostree source specs to ostree commit specs is
done using the ostree.Resolve
function. Resolving an
ostree source spec mainly involves resolving the content of the file at
<URL>/refs/heads/<REF>
.
When a manifest is serialized by calling its
Manifest.Serialize()
, it runs the
private serialize()
method on each pipeline in its array. Each pipeline in
turn creates an array of stages with the appropriate options based on the
customizations and options set on the pipeline.
The final JSON representation of the manifest that can be used with osbuild can
be created by using the standard library json.Marshal()
function.