diff --git a/internal/osbuild2/stage.go b/internal/osbuild2/stage.go index 21ad2b78d..043338695 100644 --- a/internal/osbuild2/stage.go +++ b/internal/osbuild2/stage.go @@ -91,6 +91,8 @@ func (stage *Stage) UnmarshalJSON(data []byte) error { options = new(ScriptStageOptions) case "org.osbuild.sysconfig": options = new(SysconfigStageOptions) + case "org.osbuild.sysctld": + options = new(SysctldStageOptions) case "org.osbuild.tmpfilesd": options = new(TmpfilesdStageOptions) case "org.osbuild.kernel-cmdline": diff --git a/internal/osbuild2/stage_test.go b/internal/osbuild2/stage_test.go index 6e89d3204..7f1701476 100644 --- a/internal/osbuild2/stage_test.go +++ b/internal/osbuild2/stage_test.go @@ -499,6 +499,27 @@ func TestStage_UnmarshalJSON(t *testing.T) { data: []byte(`{"type":"org.osbuild.sysconfig","options":{"kernel":{"update_default":true,"default_kernel":"kernel"},"network":{"networking":true,"no_zero_conf":true},"network-scripts":{"ifcfg":{"eth0":{"bootproto":"dhcp","device":"eth0","ipv6init":false,"onboot":true,"peerdns":true,"type":"Ethernet","userctl":true},"eth1":{"bootproto":"dhcp","device":"eth1","ipv6init":true,"onboot":true,"peerdns":true,"type":"Ethernet","userctl":false}}}}}`), }, }, + { + name: "sysctld", + fields: fields{ + Type: "org.osbuild.sysctld", + Options: &SysctldStageOptions{ + Filename: "example.conf", + Config: []SysctldConfigLine{ + { + Key: "net.ipv4.conf.*.rp_filter", + Value: "2", + }, + { + Key: "-net.ipv4.conf.all.rp_filter", + }, + }, + }, + }, + args: args{ + data: []byte(`{"type":"org.osbuild.sysctld","options":{"filename":"example.conf","config":[{"key":"net.ipv4.conf.*.rp_filter","value":"2"},{"key":"-net.ipv4.conf.all.rp_filter"}]}}`), + }, + }, { name: "systemd", fields: fields{ diff --git a/internal/osbuild2/sysctld_stage.go b/internal/osbuild2/sysctld_stage.go new file mode 100644 index 000000000..777099ff6 --- /dev/null +++ b/internal/osbuild2/sysctld_stage.go @@ -0,0 +1,66 @@ +package osbuild2 + +import ( + "encoding/json" + "fmt" + "strings" +) + +// SysctldStageOptions represents a single sysctl.d configuration file. +type SysctldStageOptions struct { + // Filename of the configuration file to be created. Must end with '.conf'. + Filename string `json:"filename"` + // List of configuration directives. The list must contain at least one item. + Config []SysctldConfigLine `json:"config"` +} + +func (SysctldStageOptions) isStageOptions() {} + +// NewSysctldStageOptions creates a new PamLimitsConf Stage options object. +func NewSysctldStageOptions(filename string, config []SysctldConfigLine) *SysctldStageOptions { + return &SysctldStageOptions{ + Filename: filename, + Config: config, + } +} + +// Unexported alias for use in SysctldStageOptions's MarshalJSON() to prevent recursion +type sysctldStageOptions SysctldStageOptions + +func (o SysctldStageOptions) MarshalJSON() ([]byte, error) { + if len(o.Config) == 0 { + return nil, fmt.Errorf("the 'Config' list must contain at least one item") + } + options := sysctldStageOptions(o) + return json.Marshal(options) +} + +// NewSysctldStage creates a new Sysctld Stage object. +func NewSysctldStage(options *SysctldStageOptions) *Stage { + return &Stage{ + Type: "org.osbuild.sysctld", + Options: options, + } +} + +// SysctldConfigLine represents a single line in a sysctl.d configuration. +type SysctldConfigLine struct { + // Kernel parameter name. + // If the string starts with "-" and the Value is not set, + // then the key is excluded from being set by a matching glob. + Key string `json:"key"` + // Kernel parameter value. + // Must be set, unless the Key value starts with "-". + Value string `json:"value,omitempty"` +} + +// Unexported alias for use in SysctldConfigLine's MarshalJSON() to prevent recursion. +type sysctldConfigLine SysctldConfigLine + +func (l SysctldConfigLine) MarshalJSON() ([]byte, error) { + if l.Value == "" && !strings.HasPrefix(l.Key, "-") { + return nil, fmt.Errorf("only Keys starting with '-' can have an empty Value") + } + line := sysctldConfigLine(l) + return json.Marshal(line) +} diff --git a/internal/osbuild2/sysctld_stage_test.go b/internal/osbuild2/sysctld_stage_test.go new file mode 100644 index 000000000..6ea98889d --- /dev/null +++ b/internal/osbuild2/sysctld_stage_test.go @@ -0,0 +1,70 @@ +package osbuild2 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewSysctldStageOptions(t *testing.T) { + filename := "example.conf" + config := []SysctldConfigLine{{ + Key: "net.ipv4.conf.default.rp_filter", + Value: "2", + }} + + expectedOptions := &SysctldStageOptions{ + Filename: filename, + Config: config, + } + actualOptions := NewSysctldStageOptions(filename, config) + assert.Equal(t, expectedOptions, actualOptions) +} + +func TestNewSysctldStage(t *testing.T) { + expectedStage := &Stage{ + Type: "org.osbuild.sysctld", + Options: &SysctldStageOptions{}, + } + actualStage := NewSysctldStage(&SysctldStageOptions{}) + assert.Equal(t, expectedStage, actualStage) +} + +func TestSysctldStageOptions_MarshalJSON_Invalid(t *testing.T) { + tests := []struct { + name string + options SysctldStageOptions + }{ + { + name: "empty-options", + options: SysctldStageOptions{}, + }, + } + for idx, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotBytes, err := json.Marshal(tt.options) + assert.NotNilf(t, err, "json.Marshal() didn't return an error, but: %s [idx: %d]", string(gotBytes), idx) + }) + } +} + +func TestSysctldConfigLine_MarshalJSON_Invalid(t *testing.T) { + tests := []struct { + name string + options SysctldConfigLine + }{ + { + name: "no-value-without-prefix", + options: SysctldConfigLine{ + Key: "key-without-prefix", + }, + }, + } + for idx, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotBytes, err := json.Marshal(tt.options) + assert.NotNilf(t, err, "json.Marshal() didn't return an error, but: %s [idx: %d]", string(gotBytes), idx) + }) + } +}