Skip to content
Closed
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
1 change: 1 addition & 0 deletions pkg/blueprint/customizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Customizations struct {
RPM *RPMCustomization `json:"rpm,omitempty" toml:"rpm,omitempty"`
RHSM *RHSMCustomization `json:"rhsm,omitempty" toml:"rhsm,omitempty"`
CACerts *CACustomization `json:"cacerts,omitempty" toml:"cacerts,omitempty"`
Firstboot []FirstbootCustomization `json:"firstboot,omitempty" toml:"firstboot,omitempty"`
}

type IgnitionCustomization struct {
Expand Down
50 changes: 50 additions & 0 deletions pkg/blueprint/firstboot_customizations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package blueprint

type FirstbootCustomization struct {
// Type of the firstboot customization. Supported values are:
// "custom", "satellite", and "aap".
Type string `json:"type,omitempty" toml:"type,omitempty"`

// Optional firstboot name. Must be unique within the blueprint and only
// alphanumeric characters with dashes and underscores are allowed.
Name string `json:"name,omitempty" toml:"name,omitempty"`

// Ignore errors when executing the firstboot script and continue with
// execution of the following firstboot scripts, if any. By default,
// firstboot scripts are executed in order and if one of them fails, the
// execution stops immediately.
IgnoreFailure bool `json:"ignore_failure,omitempty" toml:"ignore_failure,omitempty"`

// Optional CA certificate to enroll into the system before executing the
// firstboot script.
CACerts []string `json:"cacerts,omitempty" toml:"cacerts,omitempty"`

CustomFirstbootCustomization
SatelliteFirstbootCustomization
AAPFirstbootCustomization
}

type CustomFirstbootCustomization struct {
// Strings without shebang will be interpreted as shell scripts, otherwise
// the script will be executed using the shebang interpreter. Required if
// type is set to "custom".
Contents string `json:"contents,omitempty" toml:"contents,omitempty"`
}

type SatelliteFirstbootCustomization struct {
// Registration command as generated by the Satellite server. Required, if
// type is set to "satellite".
Command string `json:"command,omitempty" toml:"command,omitempty"`
}

type AAPFirstbootCustomization struct {
// Job template URL as generated by the AAP server. Required if type is set
// to "aap". Example URLs are
// https://aap.example.com/api/controller/v2/job_templates/9/callback/ or
// https://aap.example.com/api/v2/job_templates/9/callback/ depending on the
// AAP version.
JobTemplateURL string `json:"job_template_url,omitempty" toml:"job_template_url,omitempty"`

// The host config key. Required if type is set to "aap".
HostConfigKey string `json:"host_config_key,omitempty" toml:"host_config_key,omitempty"`
}
91 changes: 91 additions & 0 deletions pkg/customizations/firstboot/firstboot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package firstboot

import (
"errors"
"net/url"

"github.com/osbuild/images/pkg/blueprint"
)

type FirstbootOptions struct {
Custom []CustomFirstbootOptions
Satellite *SatelliteFirstbootOptions
AAP *AAPFirstbootOptions
}

type CustomFirstbootOptions struct {
Contents string
Name string
IgnoreFailure bool
}

type SatelliteFirstbootOptions struct {
Command string
CACerts []string
}

type AAPFirstbootOptions struct {
JobTemplateURL string
HostConfigKey string
CACerts []string
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking not having any certs would skip but being explicit is probably safer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ya that's valid, just making a note of it :)

}

func FirstbootOptionsFromBP(bpFirstboot blueprint.FirstbootCustomization) (*FirstbootOptions, error) {
var custom []CustomFirstbootOptions
for _, c := range bpFirstboot.Custom {

Check failure on line 35 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.Custom undefined (type blueprint.FirstbootCustomization has no field or method Custom)

Check failure on line 35 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.Custom undefined (type blueprint.FirstbootCustomization has no field or method Custom)

Check failure on line 35 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.Custom undefined (type blueprint.FirstbootCustomization has no field or method Custom)

Check failure on line 35 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.Custom undefined (type blueprint.FirstbootCustomization has no field or method Custom)
if c.Contents == "" {
return nil, errors.New("custom firstboot script contents cannot be empty")
}

custom = append(custom, CustomFirstbootOptions{
Contents: c.Contents,
Name: c.Name,
IgnoreFailure: c.IgnoreFailure,
})
}

var satellite *SatelliteFirstbootOptions
if bpFirstboot.Satellite != nil {

Check failure on line 48 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 48 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 48 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 48 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)
if bpFirstboot.Satellite.Command == "" {

Check failure on line 49 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 49 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 49 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 49 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)
return nil, errors.New("satellite firstboot command cannot be empty")
}

var certs []string
for _, cert := range bpFirstboot.Satellite.CACerts {

Check failure on line 54 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 54 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 54 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 54 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)
certs = append(certs, cert)
}

satellite = &SatelliteFirstbootOptions{
Command: bpFirstboot.Satellite.Command,

Check failure on line 59 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 59 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 59 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)

Check failure on line 59 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.Satellite undefined (type blueprint.FirstbootCustomization has no field or method Satellite)
CACerts: certs,
}
}

var aap *AAPFirstbootOptions
if bpFirstboot.AAP != nil {

Check failure on line 65 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 65 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 65 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 65 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)
if bpFirstboot.AAP.JobTemplateURL == "" {

Check failure on line 66 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 66 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 66 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 66 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)
return nil, errors.New("AAP firstboot job template URL cannot be empty")
}

if _, err := url.ParseRequestURI(bpFirstboot.AAP.JobTemplateURL); err != nil {

Check failure on line 70 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 70 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 70 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 70 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)
return nil, errors.New("AAP firstboot job template URL is not a valid URI")
}

var certs []string
for _, cert := range bpFirstboot.AAP.CACerts {

Check failure on line 75 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 75 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 75 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 75 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)
certs = append(certs, cert)
}

aap = &AAPFirstbootOptions{
JobTemplateURL: bpFirstboot.AAP.JobTemplateURL,

Check failure on line 80 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🛃 Container resolver tests

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 80 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / Validate manifest checksums

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 80 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / ⌨ Lint

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)

Check failure on line 80 in pkg/customizations/firstboot/firstboot.go

View workflow job for this annotation

GitHub Actions / 🔍 Check source preparation and test configs

bpFirstboot.AAP undefined (type blueprint.FirstbootCustomization has no field or method AAP)
HostConfigKey: bpFirstboot.AAP.HostConfigKey,
CACerts: certs,
}
}

return &FirstbootOptions{
Custom: custom,
Satellite: satellite,
AAP: aap,
}, nil
}
99 changes: 99 additions & 0 deletions pkg/customizations/firstboot/firstboot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package firstboot_test

import (
"testing"

"github.com/osbuild/images/pkg/blueprint"
"github.com/osbuild/images/pkg/customizations/firstboot"
"github.com/stretchr/testify/assert"
)

func TestFirstbootOptionsFromValidBP(t *testing.T) {
validBP := blueprint.FirstbootCustomization{
Custom: []blueprint.CustomFirstbootCustomization{
{Contents: "echo hello", Name: "greet"},
},
Satellite: &blueprint.SatelliteFirstbootCustomization{
Command: "satellite-command",
CACerts: []string{"cert1", "cert2"},
},
AAP: &blueprint.AAPFirstbootCustomization{
JobTemplateURL: "https://example.com/job-template",
HostConfigKey: "host-config-key",
CACerts: []string{"cert3"},
},
}

options, err := firstboot.FirstbootOptionsFromBP(validBP)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

assert.Len(t, options.Custom, 1)
assert.Equal(t, "echo hello", options.Custom[0].Contents)

assert.NotNil(t, options.Satellite)
assert.Equal(t, "satellite-command", options.Satellite.Command)
assert.ElementsMatch(t, []string{"cert1", "cert2"}, options.Satellite.CACerts)

assert.NotNil(t, options.AAP)
assert.Equal(t, "https://example.com/job-template", options.AAP.JobTemplateURL)
assert.Equal(t, "host-config-key", options.AAP.HostConfigKey)
assert.ElementsMatch(t, []string{"cert3"}, options.AAP.CACerts)
}

func TestFirstbootOptionsFromEmptyBP(t *testing.T) {
emptyBP := blueprint.FirstbootCustomization{}
options, err := firstboot.FirstbootOptionsFromBP(emptyBP)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

assert.Empty(t, options.Custom)
assert.Nil(t, options.Satellite)
assert.Nil(t, options.AAP)
}

func TestFirstbootOptionsFromBPWithEmptyCustomScript(t *testing.T) {
bpWithEmptyCustom := blueprint.FirstbootCustomization{
Custom: []blueprint.CustomFirstbootCustomization{
{Contents: "", Name: "empty-script"},
},
}

_, err := firstboot.FirstbootOptionsFromBP(bpWithEmptyCustom)
assert.Error(t, err, "expected error for empty custom script contents")
}

func TestFirstbootOptionsFromBPWithEmptySatelliteCommand(t *testing.T) {
bpWithEmptySatellite := blueprint.FirstbootCustomization{
Satellite: &blueprint.SatelliteFirstbootCustomization{
Command: "",
},
}

_, err := firstboot.FirstbootOptionsFromBP(bpWithEmptySatellite)
assert.Error(t, err, "expected error for empty satellite command")
}

func TestFirstbootOptionsFromBPWithEmptyAAPJobTemplateURL(t *testing.T) {
bpWithEmptyAAP := blueprint.FirstbootCustomization{
AAP: &blueprint.AAPFirstbootCustomization{
JobTemplateURL: "",
},
}

_, err := firstboot.FirstbootOptionsFromBP(bpWithEmptyAAP)
assert.Error(t, err, "expected error for empty AAP job template URL")
}

func TestFirstbootOptionsFromBPWithInvalidAAPJobTemplateURL(t *testing.T) {
bpWithInvalidAAP := blueprint.FirstbootCustomization{
AAP: &blueprint.AAPFirstbootCustomization{
JobTemplateURL: "not-a-valid-url",
},
}

_, err := firstboot.FirstbootOptionsFromBP(bpWithInvalidAAP)
assert.Error(t, err, "expected error for invalid AAP job template URL")
}
8 changes: 8 additions & 0 deletions pkg/distro/generic/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/osbuild/images/pkg/customizations/anaconda"
"github.com/osbuild/images/pkg/customizations/bootc"
"github.com/osbuild/images/pkg/customizations/fdo"
"github.com/osbuild/images/pkg/customizations/firstboot"
"github.com/osbuild/images/pkg/customizations/fsnode"
"github.com/osbuild/images/pkg/customizations/ignition"
"github.com/osbuild/images/pkg/customizations/kickstart"
Expand Down Expand Up @@ -302,6 +303,13 @@ func osCustomizations(t *imageType, osPackageSet rpmmd.PackageSet, options distr
osc.CACerts = ca.PEMCerts
}

if c != nil && c.Firstboot != nil {
osc.Firstboot, err = firstboot.FirstbootOptionsFromBP(*c.Firstboot)
if err != nil {
return manifest.OSCustomizations{}, fmt.Errorf("firstboot customization: %w", err)
}
}

if imageConfig.InstallWeakDeps != nil {
osc.InstallWeakDeps = *imageConfig.InstallWeakDeps
}
Expand Down
Loading
Loading