Before instantiating the manifest, any repositories that contain a package set key contained in `PayloadPackageSets()` should be added to the os package set in order to correctly generate the `org.osbuild.rpm` stage for the os pipeline. Otherwise options like GPG keys are not set correctly. Fixes #3326
588 lines
16 KiB
Go
588 lines
16 KiB
Go
package rhel7
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/exp/slices"
|
|
|
|
"github.com/osbuild/osbuild-composer/internal/blueprint"
|
|
"github.com/osbuild/osbuild-composer/internal/common"
|
|
"github.com/osbuild/osbuild-composer/internal/container"
|
|
"github.com/osbuild/osbuild-composer/internal/disk"
|
|
"github.com/osbuild/osbuild-composer/internal/distro"
|
|
"github.com/osbuild/osbuild-composer/internal/environment"
|
|
"github.com/osbuild/osbuild-composer/internal/image"
|
|
"github.com/osbuild/osbuild-composer/internal/manifest"
|
|
"github.com/osbuild/osbuild-composer/internal/osbuild"
|
|
"github.com/osbuild/osbuild-composer/internal/pathpolicy"
|
|
"github.com/osbuild/osbuild-composer/internal/platform"
|
|
"github.com/osbuild/osbuild-composer/internal/rpmmd"
|
|
"github.com/osbuild/osbuild-composer/internal/runner"
|
|
"github.com/osbuild/osbuild-composer/internal/workload"
|
|
)
|
|
|
|
const (
|
|
// package set names
|
|
|
|
// main/common os image package set name
|
|
osPkgsKey = "os"
|
|
|
|
// blueprint package set name
|
|
blueprintPkgsKey = "blueprint"
|
|
)
|
|
|
|
// RHEL-based OS image configuration defaults
|
|
var defaultDistroImageConfig = &distro.ImageConfig{
|
|
Timezone: common.ToPtr("America/New_York"),
|
|
Locale: common.ToPtr("en_US.UTF-8"),
|
|
GPGKeyFiles: []string{
|
|
"/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release",
|
|
},
|
|
Sysconfig: []*osbuild.SysconfigStageOptions{
|
|
{
|
|
Kernel: &osbuild.SysconfigKernelOptions{
|
|
UpdateDefault: true,
|
|
DefaultKernel: "kernel",
|
|
},
|
|
Network: &osbuild.SysconfigNetworkOptions{
|
|
Networking: true,
|
|
NoZeroConf: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// distribution objects without the arches > image types
|
|
var distroMap = map[string]distribution{
|
|
"rhel-7": {
|
|
name: "rhel-7",
|
|
product: "Red Hat Enterprise Linux",
|
|
osVersion: "7.9",
|
|
nick: "Maipo",
|
|
releaseVersion: "7",
|
|
modulePlatformID: "platform:el7",
|
|
vendor: "redhat",
|
|
runner: &runner.RHEL{Major: uint64(7), Minor: uint64(9)},
|
|
defaultImageConfig: defaultDistroImageConfig,
|
|
},
|
|
}
|
|
|
|
// --- Distribution ---
|
|
type distribution struct {
|
|
name string
|
|
product string
|
|
nick string
|
|
osVersion string
|
|
releaseVersion string
|
|
modulePlatformID string
|
|
vendor string
|
|
runner runner.Runner
|
|
arches map[string]distro.Arch
|
|
defaultImageConfig *distro.ImageConfig
|
|
}
|
|
|
|
func (d *distribution) Name() string {
|
|
return d.name
|
|
}
|
|
|
|
func (d *distribution) Releasever() string {
|
|
return d.releaseVersion
|
|
}
|
|
|
|
func (d *distribution) ModulePlatformID() string {
|
|
return d.modulePlatformID
|
|
}
|
|
|
|
func (d *distribution) OSTreeRef() string {
|
|
return "" // not supported
|
|
}
|
|
|
|
func (d *distribution) ListArches() []string {
|
|
archNames := make([]string, 0, len(d.arches))
|
|
for name := range d.arches {
|
|
archNames = append(archNames, name)
|
|
}
|
|
sort.Strings(archNames)
|
|
return archNames
|
|
}
|
|
|
|
func (d *distribution) GetArch(name string) (distro.Arch, error) {
|
|
arch, exists := d.arches[name]
|
|
if !exists {
|
|
return nil, errors.New("invalid architecture: " + name)
|
|
}
|
|
return arch, nil
|
|
}
|
|
|
|
func (d *distribution) addArches(arches ...architecture) {
|
|
if d.arches == nil {
|
|
d.arches = map[string]distro.Arch{}
|
|
}
|
|
|
|
// Do not make copies of architectures, as opposed to image types,
|
|
// because architecture definitions are not used by more than a single
|
|
// distro definition.
|
|
for idx := range arches {
|
|
d.arches[arches[idx].name] = &arches[idx]
|
|
}
|
|
}
|
|
|
|
func (d *distribution) isRHEL() bool {
|
|
return strings.HasPrefix(d.name, "rhel")
|
|
}
|
|
|
|
func (d *distribution) getDefaultImageConfig() *distro.ImageConfig {
|
|
return d.defaultImageConfig
|
|
}
|
|
|
|
// --- Architecture ---
|
|
|
|
type architecture struct {
|
|
distro *distribution
|
|
name string
|
|
imageTypes map[string]distro.ImageType
|
|
imageTypeAliases map[string]string
|
|
legacy string
|
|
bootType distro.BootType
|
|
}
|
|
|
|
func (a *architecture) Name() string {
|
|
return a.name
|
|
}
|
|
|
|
func (a *architecture) ListImageTypes() []string {
|
|
itNames := make([]string, 0, len(a.imageTypes))
|
|
for name := range a.imageTypes {
|
|
itNames = append(itNames, name)
|
|
}
|
|
sort.Strings(itNames)
|
|
return itNames
|
|
}
|
|
|
|
func (a *architecture) GetImageType(name string) (distro.ImageType, error) {
|
|
t, exists := a.imageTypes[name]
|
|
if !exists {
|
|
aliasForName, exists := a.imageTypeAliases[name]
|
|
if !exists {
|
|
return nil, errors.New("invalid image type: " + name)
|
|
}
|
|
t, exists = a.imageTypes[aliasForName]
|
|
if !exists {
|
|
panic(fmt.Sprintf("image type '%s' is an alias to a non-existing image type '%s'", name, aliasForName))
|
|
}
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
func (a *architecture) addImageTypes(platform platform.Platform, imageTypes ...imageType) {
|
|
if a.imageTypes == nil {
|
|
a.imageTypes = map[string]distro.ImageType{}
|
|
}
|
|
for idx := range imageTypes {
|
|
it := imageTypes[idx]
|
|
it.arch = a
|
|
it.platform = platform
|
|
a.imageTypes[it.name] = &it
|
|
for _, alias := range it.nameAliases {
|
|
if a.imageTypeAliases == nil {
|
|
a.imageTypeAliases = map[string]string{}
|
|
}
|
|
if existingAliasFor, exists := a.imageTypeAliases[alias]; exists {
|
|
panic(fmt.Sprintf("image type alias '%s' for '%s' is already defined for another image type '%s'", alias, it.name, existingAliasFor))
|
|
}
|
|
a.imageTypeAliases[alias] = it.name
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *architecture) Distro() distro.Distro {
|
|
return a.distro
|
|
}
|
|
|
|
// --- Image Type ---
|
|
type packageSetFunc func(t *imageType) rpmmd.PackageSet
|
|
|
|
type imageFunc func(workload workload.Workload, t *imageType, customizations *blueprint.Customizations, options distro.ImageOptions, packageSets map[string]rpmmd.PackageSet, containers []container.Spec, rng *rand.Rand) (image.ImageKind, error)
|
|
|
|
type imageType struct {
|
|
arch *architecture
|
|
platform platform.Platform
|
|
environment environment.Environment
|
|
name string
|
|
nameAliases []string
|
|
filename string
|
|
compression string // TODO: remove from image definition and make it a transport option
|
|
mimeType string
|
|
packageSets map[string]packageSetFunc
|
|
packageSetChains map[string][]string
|
|
defaultImageConfig *distro.ImageConfig
|
|
kernelOptions string
|
|
defaultSize uint64
|
|
buildPipelines []string
|
|
payloadPipelines []string
|
|
exports []string
|
|
image imageFunc
|
|
|
|
// bootable image
|
|
bootable bool
|
|
// If set to a value, it is preferred over the architecture value
|
|
bootType distro.BootType
|
|
// List of valid arches for the image type
|
|
basePartitionTables distro.BasePartitionTableMap
|
|
}
|
|
|
|
func (t *imageType) Name() string {
|
|
return t.name
|
|
}
|
|
|
|
func (t *imageType) Arch() distro.Arch {
|
|
return t.arch
|
|
}
|
|
|
|
func (t *imageType) Filename() string {
|
|
return t.filename
|
|
}
|
|
|
|
func (t *imageType) MIMEType() string {
|
|
return t.mimeType
|
|
}
|
|
|
|
func (t *imageType) OSTreeRef() string {
|
|
// Not supported
|
|
return ""
|
|
}
|
|
|
|
func (t *imageType) Size(size uint64) uint64 {
|
|
if size == 0 {
|
|
size = t.defaultSize
|
|
}
|
|
return size
|
|
}
|
|
|
|
func (t *imageType) BuildPipelines() []string {
|
|
return t.buildPipelines
|
|
}
|
|
|
|
func (t *imageType) PayloadPipelines() []string {
|
|
return t.payloadPipelines
|
|
}
|
|
|
|
func (t *imageType) PayloadPackageSets() []string {
|
|
return []string{blueprintPkgsKey}
|
|
}
|
|
|
|
func (t *imageType) PackageSetsChains() map[string][]string {
|
|
return t.packageSetChains
|
|
}
|
|
|
|
func (t *imageType) Exports() []string {
|
|
if len(t.exports) == 0 {
|
|
panic(fmt.Sprintf("programming error: no exports for '%s'", t.name))
|
|
}
|
|
return t.exports
|
|
}
|
|
|
|
// getBootType returns the BootType which should be used for this particular
|
|
// combination of architecture and image type.
|
|
func (t *imageType) getBootType() distro.BootType {
|
|
bootType := t.arch.bootType
|
|
if t.bootType != distro.UnsetBootType {
|
|
bootType = t.bootType
|
|
}
|
|
return bootType
|
|
}
|
|
|
|
func (t *imageType) getPartitionTable(
|
|
mountpoints []blueprint.FilesystemCustomization,
|
|
options distro.ImageOptions,
|
|
rng *rand.Rand,
|
|
) (*disk.PartitionTable, error) {
|
|
archName := t.arch.Name()
|
|
|
|
basePartitionTable, exists := t.basePartitionTables[archName]
|
|
|
|
if !exists {
|
|
return nil, fmt.Errorf("unknown arch: " + archName)
|
|
}
|
|
|
|
imageSize := t.Size(options.Size)
|
|
|
|
return disk.NewPartitionTable(&basePartitionTable, mountpoints, imageSize, true, nil, rng)
|
|
}
|
|
|
|
func (t *imageType) getDefaultImageConfig() *distro.ImageConfig {
|
|
// ensure that image always returns non-nil default config
|
|
imageConfig := t.defaultImageConfig
|
|
if imageConfig == nil {
|
|
imageConfig = &distro.ImageConfig{}
|
|
}
|
|
return imageConfig.InheritFrom(t.arch.distro.getDefaultImageConfig())
|
|
|
|
}
|
|
|
|
func (t *imageType) PartitionType() string {
|
|
archName := t.arch.Name()
|
|
basePartitionTable, exists := t.basePartitionTables[archName]
|
|
if !exists {
|
|
return ""
|
|
}
|
|
|
|
return basePartitionTable.Type
|
|
}
|
|
|
|
func (t *imageType) initializeManifest(bp *blueprint.Blueprint,
|
|
options distro.ImageOptions,
|
|
repos []rpmmd.RepoConfig,
|
|
packageSets map[string]rpmmd.PackageSet,
|
|
containers []container.Spec,
|
|
seed int64) (*manifest.Manifest, error) {
|
|
|
|
if err := t.checkOptions(bp.Customizations, options, containers); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: let image types specify valid workloads, rather than
|
|
// always assume Custom.
|
|
w := &workload.Custom{
|
|
BaseWorkload: workload.BaseWorkload{
|
|
Repos: packageSets[blueprintPkgsKey].Repositories,
|
|
},
|
|
Packages: bp.GetPackagesEx(false),
|
|
}
|
|
if services := bp.Customizations.GetServices(); services != nil {
|
|
w.Services = services.Enabled
|
|
w.DisabledServices = services.Disabled
|
|
}
|
|
|
|
source := rand.NewSource(seed)
|
|
// math/rand is good enough in this case
|
|
/* #nosec G404 */
|
|
rng := rand.New(source)
|
|
|
|
if t.image == nil {
|
|
return nil, nil
|
|
}
|
|
img, err := t.image(w, t, bp.Customizations, options, packageSets, containers, rng)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
manifest := manifest.New()
|
|
_, err = img.InstantiateManifest(&manifest, repos, t.arch.distro.runner, rng)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &manifest, err
|
|
}
|
|
|
|
func (t *imageType) Manifest(customizations *blueprint.Customizations,
|
|
options distro.ImageOptions,
|
|
repos []rpmmd.RepoConfig,
|
|
packageSets map[string][]rpmmd.PackageSpec,
|
|
containers []container.Spec,
|
|
seed int64) (distro.Manifest, error) {
|
|
|
|
bp := &blueprint.Blueprint{Name: "empty blueprint"}
|
|
err := bp.Initialize()
|
|
if err != nil {
|
|
panic("could not initialize empty blueprint: " + err.Error())
|
|
}
|
|
bp.Customizations = customizations
|
|
|
|
// the os pipeline filters repos based on the `osPkgsKey` package set, merge the repos which
|
|
// contain a payload package set into the `osPkgsKey`, so those repos are included when
|
|
// building the rpm stage in the os pipeline
|
|
// TODO: roll this into workloads
|
|
mergedRepos := make([]rpmmd.RepoConfig, 0, len(repos))
|
|
for _, repo := range repos {
|
|
for _, pkgsKey := range t.PayloadPackageSets() {
|
|
// If the repo already contains the osPkgsKey, skip
|
|
if slices.Contains(repo.PackageSets, osPkgsKey) {
|
|
break
|
|
}
|
|
if slices.Contains(repo.PackageSets, pkgsKey) {
|
|
repo.PackageSets = append(repo.PackageSets, osPkgsKey)
|
|
}
|
|
}
|
|
mergedRepos = append(mergedRepos, repo)
|
|
}
|
|
|
|
manifest, err := t.initializeManifest(bp, options, mergedRepos, nil, containers, seed)
|
|
if err != nil {
|
|
return distro.Manifest{}, err
|
|
}
|
|
|
|
return manifest.Serialize(packageSets)
|
|
}
|
|
|
|
func (t *imageType) PackageSets(bp blueprint.Blueprint, options distro.ImageOptions, repos []rpmmd.RepoConfig) map[string][]rpmmd.PackageSet {
|
|
// merge package sets that appear in the image type with the package sets
|
|
// of the same name from the distro and arch
|
|
packageSets := make(map[string]rpmmd.PackageSet)
|
|
|
|
for name, getter := range t.packageSets {
|
|
packageSets[name] = getter(t)
|
|
}
|
|
|
|
// amend with repository information
|
|
for _, repo := range repos {
|
|
if len(repo.PackageSets) > 0 {
|
|
// only apply the repo to the listed package sets
|
|
for _, psName := range repo.PackageSets {
|
|
ps := packageSets[psName]
|
|
ps.Repositories = append(ps.Repositories, repo)
|
|
packageSets[psName] = ps
|
|
}
|
|
}
|
|
}
|
|
|
|
// Similar to above, for edge-commit and edge-container, we need to set an
|
|
// ImageRef in order to properly initialize the manifest and package
|
|
// selection.
|
|
options.OSTree.ImageRef = t.OSTreeRef()
|
|
|
|
// create a temporary container spec array with the info from the blueprint
|
|
// to initialize the manifest
|
|
containers := make([]container.Spec, len(bp.Containers))
|
|
for idx := range bp.Containers {
|
|
containers[idx] = container.Spec{
|
|
Source: bp.Containers[idx].Source,
|
|
TLSVerify: bp.Containers[idx].TLSVerify,
|
|
LocalName: bp.Containers[idx].Name,
|
|
}
|
|
}
|
|
|
|
// create a manifest object and instantiate it with the computed packageSetChains
|
|
manifest, err := t.initializeManifest(&bp, options, repos, packageSets, containers, 0)
|
|
if err != nil {
|
|
// TODO: handle manifest initialization errors more gracefully, we
|
|
// refuse to initialize manifests with invalid config.
|
|
logrus.Errorf("Initializing the manifest failed for %s (%s/%s): %v", t.Name(), t.arch.distro.Name(), t.arch.Name(), err)
|
|
return nil
|
|
}
|
|
|
|
return overridePackageNamesInSets(manifest.GetPackageSetChains())
|
|
}
|
|
|
|
// Runs overridePackageNames() on each package set's Include and Exclude list
|
|
// and replaces package names.
|
|
func overridePackageNamesInSets(chains map[string][]rpmmd.PackageSet) map[string][]rpmmd.PackageSet {
|
|
pkgSetChains := make(map[string][]rpmmd.PackageSet)
|
|
for name, chain := range chains {
|
|
cc := make([]rpmmd.PackageSet, len(chain))
|
|
for idx := range chain {
|
|
cc[idx] = rpmmd.PackageSet{
|
|
Include: overridePackageNames(chain[idx].Include),
|
|
Exclude: overridePackageNames(chain[idx].Exclude),
|
|
Repositories: chain[idx].Repositories,
|
|
}
|
|
}
|
|
pkgSetChains[name] = cc
|
|
}
|
|
return pkgSetChains
|
|
}
|
|
|
|
// Resolve packages to their distro-specific name. This function is a temporary
|
|
// workaround to the issue of having packages specified outside of distros (in
|
|
// internal/manifest/os.go), which should be distro agnostic. In the future,
|
|
// this should be handled more generally.
|
|
func overridePackageNames(packages []string) []string {
|
|
for idx := range packages {
|
|
switch packages[idx] {
|
|
case "python3-pyyaml":
|
|
packages[idx] = "python3-PyYAML"
|
|
}
|
|
}
|
|
return packages
|
|
}
|
|
|
|
// checkOptions checks the validity and compatibility of options and customizations for the image type.
|
|
func (t *imageType) checkOptions(customizations *blueprint.Customizations, options distro.ImageOptions, containers []container.Spec) error {
|
|
|
|
if len(containers) > 0 {
|
|
return fmt.Errorf("embedding containers is not supported for %s on %s", t.name, t.arch.distro.name)
|
|
}
|
|
|
|
mountpoints := customizations.GetFilesystems()
|
|
|
|
err := blueprint.CheckMountpointsPolicy(mountpoints, pathpolicy.MountpointPolicies)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if osc := customizations.GetOpenSCAP(); osc != nil {
|
|
return fmt.Errorf(fmt.Sprintf("OpenSCAP unsupported os version: %s", t.arch.distro.osVersion))
|
|
}
|
|
|
|
// Check Directory/File Customizations are valid
|
|
dc := customizations.GetDirectories()
|
|
fc := customizations.GetFiles()
|
|
|
|
err = blueprint.ValidateDirFileCustomizations(dc, fc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = blueprint.CheckDirectoryCustomizationsPolicy(dc, pathpolicy.CustomDirectoriesPolicies)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = blueprint.CheckFileCustomizationsPolicy(fc, pathpolicy.CustomFilesPolicies)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// New creates a new distro object, defining the supported architectures and image types
|
|
func New() distro.Distro {
|
|
return newDistro("rhel-7")
|
|
}
|
|
|
|
func newDistro(distroName string) distro.Distro {
|
|
|
|
rd := distroMap[distroName]
|
|
|
|
// Architecture definitions
|
|
x86_64 := architecture{
|
|
name: distro.X86_64ArchName,
|
|
distro: &rd,
|
|
legacy: "i386-pc",
|
|
bootType: distro.HybridBootType,
|
|
}
|
|
|
|
x86_64.addImageTypes(
|
|
&platform.X86{
|
|
BIOS: true,
|
|
UEFIVendor: rd.vendor,
|
|
BasePlatform: platform.BasePlatform{
|
|
ImageFormat: platform.FORMAT_QCOW2,
|
|
QCOW2Compat: "0.10",
|
|
},
|
|
},
|
|
qcow2ImgType,
|
|
)
|
|
|
|
x86_64.addImageTypes(
|
|
&platform.X86{
|
|
BIOS: true,
|
|
UEFIVendor: rd.vendor,
|
|
BasePlatform: platform.BasePlatform{
|
|
ImageFormat: platform.FORMAT_VHD,
|
|
},
|
|
},
|
|
azureRhuiImgType,
|
|
)
|
|
|
|
rd.addArches(
|
|
x86_64,
|
|
)
|
|
|
|
return &rd
|
|
}
|