Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
94d3a41
worker: RunImageBuilderManifest() function
achilleas-k Oct 13, 2025
5f4232a
worker: test RunImageBuilderManifest()
achilleas-k Oct 17, 2025
543f1cf
worker: new job type: ImageBuilderManifestJob
achilleas-k Sep 25, 2025
4f19dbe
worker: job info function and handling for JobTypeImageBuilderManifest
achilleas-k Oct 14, 2025
083ccda
cloudapi: match image-builder-manifest job for manifest job results
achilleas-k Nov 10, 2025
afe5d05
osbuild-worker: add ImageBuilderManifestJob implementation
achilleas-k Oct 16, 2025
b4ff693
osbuild-worker: parse pipeline names from manifest in new job type
achilleas-k Oct 17, 2025
3b1ef63
osbuild-worker: update manifest comment to mention new job type
achilleas-k Nov 3, 2025
394108a
cloudapi: add experimental ImageBuilderManifestGeneration option
achilleas-k Oct 22, 2025
70e9b81
cloudapi: test experimental ImageBuilderManifestGeneration option
achilleas-k Oct 22, 2025
2ee0446
cloudapi: use test options struct to reduce args in newV2Server()
achilleas-k Oct 31, 2025
641b159
worker: support setting repositories for image-builder manifest
achilleas-k Oct 29, 2025
103abd5
osbuild-worker: support repo mtls configs in image-builder-manifest
achilleas-k Oct 31, 2025
4236dd9
cloudapi: log a warning when using the new manifest generation job type
achilleas-k Oct 28, 2025
8e26b65
test,CI: pass experimental options to systemd unit when deploying
achilleas-k Oct 28, 2025
0a896a6
cloudapi: test manifest handler with image-builder-manifest jobs
achilleas-k Nov 10, 2025
53d39a2
cloudapi: test metadata handler with image-builder-manifest jobs
achilleas-k Nov 12, 2025
b8d5300
worker: link repos for image-builder into root of data-dir
achilleas-k Nov 12, 2025
a38566d
cloudapi: use PipelineNames from osbuild job result instead of args
achilleas-k Nov 18, 2025
eefce60
test: dump manifests from new job type in test results
achilleas-k Nov 19, 2025
1c1f501
worker: generate manifests without librepo sources
achilleas-k Nov 19, 2025
0b5279b
worker: refactor ib-cli argument handling into functions
achilleas-k Nov 20, 2025
610e4c9
worker: support setting subscription options for image-builder manifest
achilleas-k Nov 20, 2025
dbe3635
test/common: change expected RHSM fact value when using image-builder
achilleas-k Dec 1, 2025
fe89303
test/common: skip checking OSCAP RHSM facts when using image-builder
achilleas-k Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,13 @@ API:
- aws/rhel-10.1-ga-aarch64
INTERNAL_NETWORK: ["true"]

# single test config for experimental feature
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(super nitpick) I wonder if this and the previous one (or even also the previous-previous one) commits could be combined but very subjective and fine as is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm going to add repository handling here and I need to fix up the test to install ib-cli, so I'll do some history rewriting to reduce the commit count, sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

- IMAGE_TYPE: ["aws"]
RUNNER:
- aws/rhel-10.1-ga-x86_64
IMAGE_BUILDER_EXPERIMENTAL: "image-builder-manifest-generation=1"
INTERNAL_NETWORK: ["true"]

API-module-hotfixes:
stage: test
extends: .terraform
Expand Down
7 changes: 7 additions & 0 deletions cmd/osbuild-composer/composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/osbuild/images/pkg/depsolvednf"
"github.com/osbuild/images/pkg/distrofactory"
"github.com/osbuild/images/pkg/experimentalflags"
"github.com/osbuild/images/pkg/reporegistry"
"github.com/osbuild/osbuild-composer/internal/auth"
"github.com/osbuild/osbuild-composer/internal/cloudapi"
Expand Down Expand Up @@ -173,6 +174,12 @@ func (c *Composer) InitAPI(cert, key string, enableTLS bool, enableMTLS bool, en
TenantProviderFields: c.config.Koji.JWTTenantProviderFields,
}

// handle experimental image-builder manifest generation option using the
// experimentalflags pkg from osbuild/images.
if experimentalflags.Bool("image-builder-manifest-generation") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(super nitpick) I personally would have combined this with the previous commit that added ImageBuilderManifestGeneration to the config but up to you how granular you want this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

config.ImageBuilderManifestGeneration = true
}

c.api = cloudapi.NewServer(c.workers, c.distros, c.repos, config)

if !enableTLS {
Expand Down
1 change: 1 addition & 0 deletions cmd/osbuild-worker/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var (
WorkerClientErrorFrom = workerClientErrorFrom
MakeJobErrorFromOsbuildOutput = makeJobErrorFromOsbuildOutput
Main = main
ParseManifestPipelines = parseManifestPipelines
)

func MockRun(new func()) (restore func()) {
Expand Down
136 changes: 136 additions & 0 deletions cmd/osbuild-worker/jobimpl-image-builder-manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/osbuild/osbuild-composer/internal/common"
"github.com/osbuild/osbuild-composer/internal/worker"
"github.com/osbuild/osbuild-composer/internal/worker/clienterrors"
"github.com/sirupsen/logrus"
)

type ImageBuilderManifestJobImpl struct {
RepositoryMTLSConfig *RepositoryMTLSConfig
}

func (impl *ImageBuilderManifestJobImpl) Run(job worker.Job) error {
logWithId := logrus.WithField("jobId", job.Id().String())

result := &worker.ImageBuilderManifestJobResult{
Manifest: nil,
ManifestInfo: worker.ManifestInfo{
OSBuildComposerVersion: common.BuildVersion(),
// TODO: add image-builder version fields when we get
// machine-readable version output from image-builder-cli
},
}

defer func() {
err := job.Finish(&result)
if err != nil {
logWithId.Errorf("Error reporting job result: %v", err)
}
}()

var args worker.ImageBuilderManifestJob
err := job.Args(&args)
if err != nil {
return err
}

if impl.RepositoryMTLSConfig != nil {
for repoi, repo := range args.Args.Repositories {
for _, baseurlstr := range repo.BaseURLs {
match, err := impl.RepositoryMTLSConfig.CompareBaseURL(baseurlstr)
if err != nil {
result.JobError = clienterrors.New(clienterrors.ErrorInvalidRepositoryURL, "Repository URL is malformed", err.Error())
return err
}
if match {
impl.RepositoryMTLSConfig.SetupRepoSSL(&args.Args.Repositories[repoi])
}
}
}
}

manifest, err := worker.RunImageBuilderManifest(args.Args, args.ExtraEnv, os.Stderr)
if err != nil {
result.JobError = workerClientErrorFrom(err, logWithId)
}
result.Manifest = manifest

pipelineNames, err := parseManifestPipelines(manifest)
if err != nil {
// Not a critical failure. Log the error and continue. This will cause
// the job log to be unordered.
// For Koji builds, it will also fail to create package listings.
logWithId.Warningf("failed to parse manifest for pipeline names: %v", err)
}
result.ManifestInfo.PipelineNames = pipelineNames

return nil
}

// Parse the raw manifest into an incomplete representation of the manifest
// structure in order to retrieve the pipeline names in order. Any pipeline
// names that appear under the 'build' property of another pipeline are added
// to the build pipelines list. The rest are added to the payload pipelines.
func parseManifestPipelines(rawManifest []byte) (*worker.PipelineNames, error) {
if len(rawManifest) == 0 {
return nil, fmt.Errorf("manifest is empty")
}

// Partial manifest structure for extracting pipeline names.
type partialManifest struct {
Version string `json:"version"`
Pipelines []struct {
Name string `json:"name"`
Build string `json:"build"`
} `json:"pipelines"`
}

var pm partialManifest
if err := json.Unmarshal(rawManifest, &pm); err != nil {
return nil, err
}

if pm.Version != "2" {
return nil, fmt.Errorf("unexpected manifest version: %s != 2", pm.Version)
}

if len(pm.Pipelines) == 0 {
return nil, fmt.Errorf("no pipelines found")
}

// Collect all pipeline names in order and save build pipeline names in a
// map so we can split them later.
var allPipelineNames []string
buildPipelineNames := make(map[string]bool)
for _, pl := range pm.Pipelines {
allPipelineNames = append(allPipelineNames, pl.Name)
if pl.Build != "" {
// The build property is in the form "name:build", where "build" is
// the name of the build pipeline.
parts := strings.SplitN(pl.Build, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("unexpected pipeline build property format: %s", pl.Build)
}
buildPipelineNames[parts[1]] = true
}
}

var pipelineNames worker.PipelineNames
for _, plName := range allPipelineNames {
if buildPipelineNames[plName] {
pipelineNames.Build = append(pipelineNames.Build, plName)
continue
}

pipelineNames.Payload = append(pipelineNames.Payload, plName)
}

return &pipelineNames, nil
}
218 changes: 218 additions & 0 deletions cmd/osbuild-worker/jobimpl-image-builder-manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package main_test

import (
"testing"

main "github.com/osbuild/osbuild-composer/cmd/osbuild-worker"
"github.com/osbuild/osbuild-composer/internal/worker"
"github.com/stretchr/testify/assert"
)

func TestParseManifestPipelines(t *testing.T) {
type testCase struct {
Manifest []byte

ExpectedPipelines *worker.PipelineNames
ExpectedError string
}

testCases := map[string]testCase{
"empty": {
ExpectedError: "manifest is empty",
},
"bad-json": {
Manifest: []byte(`{not json}`),
ExpectedError: "invalid character 'n' looking for beginning of object key string",
},
"bad-manifest": {
Manifest: []byte(`{"unknown-key": "value"}`),
ExpectedError: `unexpected manifest version: != 2`,
},
"bad-version": {
Manifest: []byte(`{"version": "42"}`),
ExpectedError: `unexpected manifest version: 42 != 2`,
},
"no-pipelines": {
Manifest: []byte(`{"version": "2"}`),
ExpectedError: `no pipelines found`,
},
"bad-build": {
Manifest: []byte(`{
"version": "2",
"pipelines": [
{
"name": "build-pipeline"
},
{
"name": "root-tree",
"build": "build-pipeline"
}
],
"sources": []
}`),
ExpectedError: `unexpected pipeline build property format: build-pipeline`,
},

"simple": {
Manifest: []byte(`{
"version": "2",
"pipelines": [
{
"name": "pipeline"
}
],
"sources": []
}`),
ExpectedPipelines: &worker.PipelineNames{
Payload: []string{"pipeline"},
},
},

"with-build": {
Manifest: []byte(`{
"version": "2",
"pipelines": [
{
"name": "build-pipeline"
},
{
"name": "root-tree",
"build": "name:build-pipeline"
},
{
"name": "image",
"build": "name:build-pipeline"
}
],
"sources": []
}`),
ExpectedPipelines: &worker.PipelineNames{
Build: []string{"build-pipeline"},
Payload: []string{"root-tree", "image"},
},
},

"real-simplified": { // Real manifest, but simplified by removing stage options, inputs, and sources
Manifest: []byte(`{
"version": "2",
"pipelines": [
{
"name": "build",
"runner": "org.osbuild.fedora42",
"stages": [
{
"type": "org.osbuild.rpm",
"inputs": {},
"options": {}
},
{
"type": "org.osbuild.selinux",
"options": {}
}
]
},
{
"name": "os",
"build": "name:build",
"stages": [
{
"type": "org.osbuild.rpm",
"inputs": {},
"options": {}
},
{
"type": "org.osbuild.fix-bls",
"options": {}
},
{
"type": "org.osbuild.locale",
"options": {
"language": "C.UTF-8"
}
},
{
"type": "org.osbuild.hostname",
"options": {}
},
{
"type": "org.osbuild.timezone",
"options": {}
},
{
"type": "org.osbuild.machine-id",
"options": {}
}
]
},
{
"name": "archive",
"build": "name:build",
"stages": [
{
"type": "org.osbuild.tar",
"inputs": {},
"options": {}
}
]
},
{
"name": "xz",
"build": "name:build",
"stages": [
{
"type": "org.osbuild.xz",
"inputs": {},
"options": {}
}
]
}
],
"sources": {
"org.osbuild.librepo": {}
}
}`),
ExpectedPipelines: &worker.PipelineNames{
Build: []string{"build"},
Payload: []string{"os", "archive", "xz"},
},
},
"unknown-build": {
// The build property refers to a pipeline that doesn't exist. This
// manifest is invalid, so it will fail to validate in osbuild, but
// our function will work and ignore the invalid reference. This
// means the "build-pipeline" will not be identified as a build
// pipeline but instead be added to the Payload list along with
// "root-tree".
Manifest: []byte(`{
"version": "2",
"pipelines": [
{
"name": "build-pipeline"
},
{
"name": "root-tree",
"build": "build:not-a-pipeline"
}
],
"sources": []
}`),
ExpectedPipelines: &worker.PipelineNames{
Payload: []string{"build-pipeline", "root-tree"},
},
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
plNames, err := main.ParseManifestPipelines(tc.Manifest)
if tc.ExpectedError != "" {
assert.EqualError(err, tc.ExpectedError)
return
}

assert.NoError(err)
assert.Equal(tc.ExpectedPipelines, plNames)
})
}
}
Loading
Loading