diff --git a/internal/osbuild2/stage.go b/internal/osbuild2/stage.go index 79261e8e3..4cf95d6f8 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.tmpfilesd": + options = new(TmpfilesdStageOptions) case "org.osbuild.kernel-cmdline": options = new(KernelCmdlineStageOptions) case "org.osbuild.rpm": diff --git a/internal/osbuild2/stage_test.go b/internal/osbuild2/stage_test.go index 6da0c425c..074764992 100644 --- a/internal/osbuild2/stage_test.go +++ b/internal/osbuild2/stage_test.go @@ -526,6 +526,24 @@ func TestStage_UnmarshalJSON(t *testing.T) { data: []byte(`{"type":"org.osbuild.timezone","options":{"zone":""}}`), }, }, + { + name: "tmpfilesd", + fields: fields{ + Type: "org.osbuild.tmpfilesd", + Options: &TmpfilesdStageOptions{ + Filename: "example.conf", + Config: []TmpfilesdConfigLine{ + { + Type: "d", + Path: "/tmp/my-example-path", + }, + }, + }, + }, + args: args{ + data: []byte(`{"type":"org.osbuild.tmpfilesd","options":{"filename":"example.conf","config":[{"type":"d","path":"/tmp/my-example-path"}]}}`), + }, + }, { name: "users", fields: fields{ diff --git a/internal/osbuild2/tmpfilesd_stage.go b/internal/osbuild2/tmpfilesd_stage.go new file mode 100644 index 000000000..bcbfdf93b --- /dev/null +++ b/internal/osbuild2/tmpfilesd_stage.go @@ -0,0 +1,61 @@ +package osbuild2 + +import ( + "encoding/json" + "fmt" +) + +// TmpfilesdStageOptions represents a single tmpfiles.d configuration file. +type TmpfilesdStageOptions 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 []TmpfilesdConfigLine `json:"config"` +} + +func (TmpfilesdStageOptions) isStageOptions() {} + +// NewTmpfilesdStageOptions creates a new Tmpfilesd Stage options object. +func NewTmpfilesdStageOptions(filename string, config []TmpfilesdConfigLine) *TmpfilesdStageOptions { + return &TmpfilesdStageOptions{ + Filename: filename, + Config: config, + } +} + +// Unexported alias for use in TmpfilesdStageOptions's MarshalJSON() to prevent recursion +type tmpfilesdStageOptions TmpfilesdStageOptions + +func (o TmpfilesdStageOptions) MarshalJSON() ([]byte, error) { + if len(o.Config) == 0 { + return nil, fmt.Errorf("the 'Config' list must contain at least one item") + } + options := tmpfilesdStageOptions(o) + return json.Marshal(options) +} + +// NewTmpfilesdStage creates a new Tmpfilesd Stage object. +func NewTmpfilesdStage(options *TmpfilesdStageOptions) *Stage { + return &Stage{ + Type: "org.osbuild.tmpfilesd", + Options: options, + } +} + +// TmpfilesdConfigLine represents a single line in a tmpfiles.d configuration. +type TmpfilesdConfigLine struct { + // The file system path type + Type string `json:"type"` + // Absolute file system path + Path string `json:"path"` + // The file access mode when creating the file or directory + Mode string `json:"mode,omitempty"` + // The user to use for the file or directory + User string `json:"user,omitempty"` + // The group to use for the file or directory + Group string `json:"group,omitempty"` + // Date field used to decide what files to delete when cleaning + Age string `json:"age,omitempty"` + // Argument with its meaning being specific to the path type + Argument string `json:"argument,omitempty"` +} diff --git a/internal/osbuild2/tmpfilesd_stage_test.go b/internal/osbuild2/tmpfilesd_stage_test.go new file mode 100644 index 000000000..f2562ca54 --- /dev/null +++ b/internal/osbuild2/tmpfilesd_stage_test.go @@ -0,0 +1,50 @@ +package osbuild2 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewTmpfilesdStageOptions(t *testing.T) { + filename := "example.conf" + config := []TmpfilesdConfigLine{{ + Type: "d", + Path: "/tmp/my-example-path", + }} + + expectedOptions := &TmpfilesdStageOptions{ + Filename: filename, + Config: config, + } + actualOptions := NewTmpfilesdStageOptions(filename, config) + assert.Equal(t, expectedOptions, actualOptions) +} + +func TestNewTmpfilesdStage(t *testing.T) { + expectedStage := &Stage{ + Type: "org.osbuild.tmpfilesd", + Options: &TmpfilesdStageOptions{}, + } + actualStage := NewTmpfilesdStage(&TmpfilesdStageOptions{}) + assert.Equal(t, expectedStage, actualStage) +} + +func TestTmpfilesdStageOptions_MarshalJSON_Invalid(t *testing.T) { + tests := []struct { + name string + options TmpfilesdStageOptions + }{ + { + name: "empty-options", + options: TmpfilesdStageOptions{}, + }, + } + 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) + }) + } +}