Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Schutzfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"common": {
"rngseed": 13,
"rngseed": 15,
"bootc-image-builder": {
"ref": "quay.io/centos-bootc/bootc-image-builder@sha256:9893e7209e5f449b86ababfd2ee02a58cca2e5990f77b06c3539227531fc8120"
},
Expand Down
2 changes: 2 additions & 0 deletions cmd/check-host-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func init() {
RequiresBlueprint: true,
RequiresCustomizations: true,
TempDisabled: "",
RunOn: []string{"centos", "!rhel"},
}, usersCheck)
}
```
Expand All @@ -31,6 +32,7 @@ Metadata fields have the following semantics:
* **RequiresBlueprint**: when set to true and the blueprint is empty, the check is automatically skipped.
* **RequiresCustomizations**: when set to true and config, blueprint, or customizations is nil, the check is skipped.
* **TempDisabled**: the check is temporarily disabled (skipped) when this is not an empty string (e.g. issue URL).
* **RunOn**: when set, run on specific distro IDs (or use bang to exclude specific OS).

Checks can return:

Expand Down
9 changes: 5 additions & 4 deletions cmd/check-host-config/check/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import (
// Metadata provides information about a check. It is used to manage the execution
// of the check and to provide context in logs and reports.
type Metadata struct {
Name string // Name of the check (used for lookup and logging)
RequiresBlueprint bool // Ensure Blueprint is not nil, skip the check otherwise
RequiresCustomizations bool // Ensure Customizations is not nil, skip the check otherwise
TempDisabled string // Set to non-empty string with URL to issue tracker to disable the check temporarily
Name string // Name of the check (used for lookup and logging)
RequiresBlueprint bool // Ensure Blueprint is not nil, skip the check otherwise
RequiresCustomizations bool // Ensure Customizations is not nil, skip the check otherwise
TempDisabled string // Set to non-empty string with URL to issue tracker to disable the check temporarily
RunOn []string // List of OS IDs to run the check on (prefix with `!` to exclude)
}

// CheckFunc is the function type that all checks must implement.
Expand Down
27 changes: 13 additions & 14 deletions cmd/check-host-config/check/modularity.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func init() {
RegisterCheck(Metadata{
Name: "modularity",
RequiresBlueprint: true,
TempDisabled: "https://github.com/osbuild/images/issues/2061",
RunOn: []string{"centos-9"},
}, modularityCheck)
}

Expand Down Expand Up @@ -45,30 +45,29 @@ func modularityCheck(meta *Metadata, config *buildconfig.BuildConfig) error {

log.Println("Checking enabled modules")

// Get list of enabled modules from dnf (use -q to suppress download progress output)
stdout, _, _, err := Exec("dnf", "-q", "module", "list", "--enabled")
// Get list of enabled modules from dnf (use -y for non-interactive, -q to suppress download progress output)
stdout, _, _, err := Exec("dnf", "-y", "-q", "module", "list", "--enabled")
if err != nil {
return Fail("failed to list enabled modules:", err)
}

// Parse dnf output: skip first 3 lines (header) and last 2 lines (footer)
// Parse dnf output: detect table rows dynamically (lines with at least 3 columns)
lines := strings.Split(string(stdout), "\n")
if len(lines) < 5 {
return Fail("dnf module list returned nothing")
}

enabledModules := make(map[string]bool)
for i := 3; i < len(lines)-2; i++ {
line := strings.TrimSpace(lines[i])
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Format: "name stream" or "name stream"
fields := strings.Fields(line)
if len(fields) >= 2 {
moduleKey := fields[0] + ":" + fields[1]
enabledModules[moduleKey] = true
if len(fields) < 3 {
continue
}
moduleKey := fields[0] + ":" + fields[1]
enabledModules[moduleKey] = true
}
if len(enabledModules) == 0 {
return Fail("dnf module list returned nothing")
}

for _, expected := range expectedModules {
Expand Down
121 changes: 68 additions & 53 deletions cmd/check-host-config/check/modularity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,56 +10,62 @@ import (
"github.com/stretchr/testify/require"
)

// dnf module list --enabled output fixtures for different RHEL versions
func dnfModuleListOutputRHEL7() string {
return "Last metadata expiration check: 1:23:45 ago on Mon 01 Jan 2024 12:00:00 PM UTC.\n" +
"Dependencies resolved.\n" +
"Module Stream Profiles\n" +
"nodejs 10 [e] common [d], development, minimal\n" +
"python36 3.6 [e] build, common [d], devel\n" +
"Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive\n"
}
// dnf module list --enabled output fixtures for different distro versions
var (
dnfModuleListOutputRHEL7 = `Last metadata expiration check: 1:23:45 ago on Mon 01 Jan 2024 12:00:00 PM UTC.
Dependencies resolved.
Module Stream Profiles
nodejs 10 [e] common [d], development, minimal
python36 3.6 [e] build, common [d], devel
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive
`

func dnfModuleListOutputRHEL8() string {
return "Last metadata expiration check: 0:00:00 ago on Mon 01 Jan 2024 12:00:00 PM UTC.\n" +
"Dependencies resolved.\n" +
"Module Stream Profiles\n" +
"nodejs 12 [e] common [d], development, minimal, s2i\n" +
"python38 3.8 [e] build, common [d], devel, minimal\n" +
"postgresql 12 [e] client, server [d]\n" +
"Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive\n"
}
dnfModuleListOutputRHEL8 = `Last metadata expiration check: 0:00:00 ago on Mon 01 Jan 2024 12:00:00 PM UTC.
Dependencies resolved.
Module Stream Profiles
nodejs 12 [e] common [d], development, minimal, s2i
python38 3.8 [e] build, common [d], devel, minimal
postgresql 12 [e] client, server [d]
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive
`

func dnfModuleListOutputRHEL9() string {
return "Last metadata expiration check: 0:00:00 ago\n" +
"Dependencies resolved.\n" +
"Module Stream Profiles\n" +
"nodejs 18 [d] common [d], development, minimal, s2i\n" +
"python39 3.9 [d] build, common [d], devel, minimal\n" +
"Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive\n"
}
dnfModuleListOutputRHEL9 = `Last metadata expiration check: 0:00:00 ago
Dependencies resolved.
Module Stream Profiles
nodejs 18 [d] common [d], development, minimal, s2i
python39 3.9 [d] build, common [d], devel, minimal
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive
`

func dnfModuleListOutputRHEL10() string {
return "Last metadata expiration check: 0:00:00 ago\n" +
"Dependencies resolved.\n" +
"Module Stream Profiles\n" +
"nodejs 20 [e] common [d], development, minimal, s2i\n" +
"python312 3.12 [e] build, common [d], devel, minimal\n" +
"postgresql 16 [e] client, server [d], devel\n" +
"Use \"dnf module info <module:stream>\" to get more information.\n" +
"Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive\n"
}
dnfModuleListOutputRHEL10 = `Last metadata expiration check: 0:00:00 ago
Dependencies resolved.
Module Stream Profiles
nodejs 20 [e] common [d], development, minimal, s2i
python312 3.12 [e] build, common [d], devel, minimal
postgresql 16 [e] client, server [d], devel
Use "dnf module info <module:stream>" to get more information.
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive
`

func dnfModuleListOutputMultiple() string {
return "Last metadata expiration check: 0:00:00 ago\n" +
"Dependencies resolved.\n" +
"Module Stream Profiles\n" +
"nodejs 18 [e] common [d], development, minimal, s2i\n" +
"python39 3.9 [e] build, common [d], devel, minimal\n" +
"postgresql 13 [e] client, server [d]\n" +
"ruby 3.1 [e] common [d], devel\n" +
"Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive\n"
}
dnfModuleListOutputCentOS9 = `CentOS Stream 9 - AppStream
Name Stream Profiles Summary
nodejs 18 [e] common [d], development, minimal, s2i Javascript runtime

Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled
`

dnfModuleListOutputMultiple = `Last metadata expiration check: 0:00:00 ago
Dependencies resolved.
Module Stream Profiles
nodejs 18 [e] common [d], development, minimal, s2i
python39 3.9 [e] build, common [d], devel, minimal
postgresql 13 [e] client, server [d]
ruby 3.1 [e] common [d], devel
Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled, [a]ctive
`
)

const dnfModuleListCmd = "dnf -y -q module list --enabled"

func TestModularityCheck(t *testing.T) {
tests := []struct {
Expand All @@ -79,7 +85,7 @@ func TestModularityCheck(t *testing.T) {
{Name: "nodejs", Stream: "18"},
},
mockExec: map[string]ExecResult{
"dnf -q module list --enabled": {Stdout: []byte(dnfModuleListOutputRHEL9())},
dnfModuleListCmd: {Stdout: []byte(dnfModuleListOutputRHEL9)},
},
},
{
Expand All @@ -89,7 +95,7 @@ func TestModularityCheck(t *testing.T) {
{Name: "python36", Stream: "3.6"},
},
mockExec: map[string]ExecResult{
"dnf -q module list --enabled": {Stdout: []byte(dnfModuleListOutputRHEL7())},
dnfModuleListCmd: {Stdout: []byte(dnfModuleListOutputRHEL7)},
},
},
{
Expand All @@ -100,7 +106,7 @@ func TestModularityCheck(t *testing.T) {
{Name: "postgresql", Stream: "12"},
},
mockExec: map[string]ExecResult{
"dnf -q module list --enabled": {Stdout: []byte(dnfModuleListOutputRHEL8())},
dnfModuleListCmd: {Stdout: []byte(dnfModuleListOutputRHEL8)},
},
},
{
Expand All @@ -110,7 +116,7 @@ func TestModularityCheck(t *testing.T) {
{Name: "python39", Stream: "3.9"},
},
mockExec: map[string]ExecResult{
"dnf -q module list --enabled": {Stdout: []byte(dnfModuleListOutputRHEL9())},
dnfModuleListCmd: {Stdout: []byte(dnfModuleListOutputRHEL9)},
},
},
{
Expand All @@ -121,7 +127,16 @@ func TestModularityCheck(t *testing.T) {
{Name: "postgresql", Stream: "16"},
},
mockExec: map[string]ExecResult{
"dnf -q module list --enabled": {Stdout: []byte(dnfModuleListOutputRHEL10())},
dnfModuleListCmd: {Stdout: []byte(dnfModuleListOutputRHEL10)},
},
},
{
name: "pass CentOS 9 format",
config: []blueprint.EnabledModule{
{Name: "nodejs", Stream: "18"},
},
mockExec: map[string]ExecResult{
dnfModuleListCmd: {Stdout: []byte(dnfModuleListOutputCentOS9)},
},
},
{
Expand All @@ -133,7 +148,7 @@ func TestModularityCheck(t *testing.T) {
{Name: "ruby", Stream: "3.1"},
},
mockExec: map[string]ExecResult{
"dnf -q module list --enabled": {Stdout: []byte(dnfModuleListOutputMultiple())},
dnfModuleListCmd: {Stdout: []byte(dnfModuleListOutputMultiple)},
},
},
{
Expand All @@ -142,7 +157,7 @@ func TestModularityCheck(t *testing.T) {
{Name: "nodejs", Stream: "18"},
},
mockExec: map[string]ExecResult{
"dnf -q module list --enabled": {Code: 1, Err: errors.New("dnf failed")},
dnfModuleListCmd: {Code: 1, Err: errors.New("dnf failed")},
},
wantErr: check.ErrCheckFailed,
},
Expand Down
34 changes: 32 additions & 2 deletions cmd/check-host-config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"log"
"os"
"slices"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -37,8 +38,30 @@ func waitForSystem(timeout time.Duration) error {
return nil
}

// shouldRunOn returns whether a check should run on the current system.
func shouldRunOn(osRelease *check.OSRelease, runOn []string) bool {
if len(runOn) == 0 || osRelease == nil {
return true
}

currentID := strings.ToLower(strings.TrimSpace(osRelease.ID + "-" + osRelease.VersionID))
var inclusions []string
for _, entry := range runOn {
entry = strings.TrimSpace(entry)
if after, ok := strings.CutPrefix(entry, "!"); ok {
if strings.ToLower(after) == currentID {
return false
}
} else {
inclusions = append(inclusions, strings.ToLower(entry))
}
}

return len(inclusions) == 0 || slices.Contains(inclusions, currentID)
}

// runChecks runs all checks sequentially and processes their results.
func runChecks(checks []check.RegisteredCheck, config *buildconfig.BuildConfig, quiet bool) bool {
func runChecks(checks []check.RegisteredCheck, config *buildconfig.BuildConfig, osRelease *check.OSRelease, quiet bool) bool {
defer log.SetPrefix("")
if quiet {
log.SetOutput(io.Discard)
Expand All @@ -52,6 +75,8 @@ func runChecks(checks []check.RegisteredCheck, config *buildconfig.BuildConfig,
log.SetPrefix(meta.Name + ": ")

switch {
case !shouldRunOn(osRelease, meta.RunOn):
err = check.Skip(osRelease.ID + "-" + osRelease.VersionID + " excluded via RunOn: " + strings.Join(meta.RunOn, ", "))
case meta.TempDisabled != "":
err = check.Skip("temporarily disabled: " + meta.TempDisabled)
case meta.RequiresBlueprint && (config == nil || config.Blueprint == nil):
Expand Down Expand Up @@ -113,7 +138,12 @@ func main() {
log.Fatalf("Problem during waiting for system to be running: %v\n", err)
}

if !runChecks(checks, config, *quiet) {
osRelease, _ := check.ParseOSRelease("")
if osRelease == nil {
log.Println("Could not parse /etc/os-release, RunOn filtering disabled")
}

if !runChecks(checks, config, osRelease, *quiet) {
log.Fatalf("Host check with config %q failed, return code 1\n", configFile)
}
}
Loading
Loading