diff --git a/internal/osbuild2/cloud_init_stage.go b/internal/osbuild2/cloud_init_stage.go new file mode 100644 index 000000000..4e1858b69 --- /dev/null +++ b/internal/osbuild2/cloud_init_stage.go @@ -0,0 +1,73 @@ +package osbuild2 + +import ( + "encoding/json" + "fmt" +) + +type CloudInitStageOptions struct { + ConfigFiles map[string]CloudInitConfigFile `json:"configuration_files,omitempty"` +} + +func (CloudInitStageOptions) isStageOptions() {} + +func NewCloudInitStage(options *CloudInitStageOptions) *Stage { + return &Stage{ + Type: "org.osbuild.cloud-init", + Options: options, + } +} + +// Represents a cloud-init configuration file +type CloudInitConfigFile struct { + SystemInfo *CloudInitConfigSystemInfo `json:"system_info,omitempty"` +} + +// Unexported struct for use in CloudInitConfigFile's MarshalJSON() to prevent recursion +type cloudInitConfigFile struct { + SystemInfo *CloudInitConfigSystemInfo `json:"system_info,omitempty"` +} + +func (c CloudInitConfigFile) MarshalJSON() ([]byte, error) { + if c.SystemInfo == nil { + return nil, fmt.Errorf("at least one cloud-init configuration option must be specified") + } + configFile := cloudInitConfigFile(c) + return json.Marshal(configFile) +} + +// Represents the 'system_info' configuration section +type CloudInitConfigSystemInfo struct { + DefaultUser *CloudInitConfigDefaultUser `json:"default_user,omitempty"` +} + +// Unexported struct for use in CloudInitConfigSystemInfo's MarshalJSON() to prevent recursion +type cloudInitConfigSystemInfo struct { + DefaultUser *CloudInitConfigDefaultUser `json:"default_user,omitempty"` +} + +func (si CloudInitConfigSystemInfo) MarshalJSON() ([]byte, error) { + if si.DefaultUser == nil { + return nil, fmt.Errorf("at least one configuration option must be specified for 'system_info' section") + } + systemInfo := cloudInitConfigSystemInfo(si) + return json.Marshal(systemInfo) +} + +// Configuration of the 'default' user created by cloud-init. +type CloudInitConfigDefaultUser struct { + Name string `json:"name,omitempty"` +} + +// Unexported struct for use in CloudInitConfigDefaultUser's MarshalJSON() to prevent recursion +type cloudInitConfigDefaultUser struct { + Name string `json:"name,omitempty"` +} + +func (du CloudInitConfigDefaultUser) MarshalJSON() ([]byte, error) { + if du.Name == "" { + return nil, fmt.Errorf("at least one configuration option must be specified for 'default_user' section") + } + defaultUser := cloudInitConfigDefaultUser(du) + return json.Marshal(defaultUser) +} diff --git a/internal/osbuild2/cloud_init_stage_test.go b/internal/osbuild2/cloud_init_stage_test.go new file mode 100644 index 000000000..3db7a8a34 --- /dev/null +++ b/internal/osbuild2/cloud_init_stage_test.go @@ -0,0 +1,61 @@ +package osbuild2 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewCloudInitStage(t *testing.T) { + expectedStage := &Stage{ + Type: "org.osbuild.cloud-init", + Options: &CloudInitStageOptions{}, + } + actualStage := NewCloudInitStage(&CloudInitStageOptions{}) + assert.Equal(t, expectedStage, actualStage) +} + +func TestCloudInitStage_MarshalJSON_Invalid(t *testing.T) { + tests := []struct { + name string + options CloudInitStageOptions + }{ + { + name: "no-config-file-section", + options: CloudInitStageOptions{ + ConfigFiles: map[string]CloudInitConfigFile{ + "00-default_user.cfg": {}, + }, + }, + }, + { + name: "no-system-info-section-option", + options: CloudInitStageOptions{ + ConfigFiles: map[string]CloudInitConfigFile{ + "00-default_user.cfg": { + SystemInfo: &CloudInitConfigSystemInfo{}, + }, + }, + }, + }, + { + name: "no-default-user-section-option", + options: CloudInitStageOptions{ + ConfigFiles: map[string]CloudInitConfigFile{ + "00-default_user.cfg": { + SystemInfo: &CloudInitConfigSystemInfo{ + DefaultUser: &CloudInitConfigDefaultUser{}, + }, + }, + }, + }, + }, + } + 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) + }) + } +} diff --git a/internal/osbuild2/stage.go b/internal/osbuild2/stage.go index a9151ac26..cfb0dda90 100644 --- a/internal/osbuild2/stage.go +++ b/internal/osbuild2/stage.go @@ -85,6 +85,8 @@ func (stage *Stage) UnmarshalJSON(data []byte) error { options = new(GroupsStageOptions) case "org.osbuild.timezone": options = new(TimezoneStageOptions) + case "org.osbuild.cloud-init": + options = new(CloudInitStageOptions) case "org.osbuild.chrony": options = new(ChronyStageOptions) case "org.osbuild.dracut": diff --git a/internal/osbuild2/stage_test.go b/internal/osbuild2/stage_test.go index deabe5b61..3c0a1bd29 100644 --- a/internal/osbuild2/stage_test.go +++ b/internal/osbuild2/stage_test.go @@ -52,6 +52,36 @@ func TestStage_UnmarshalJSON(t *testing.T) { }, wantErr: true, }, + { + name: "cloud-init", + fields: fields{ + Type: "org.osbuild.cloud-init", + Options: &CloudInitStageOptions{}, + }, + args: args{ + data: []byte(`{"type":"org.osbuild.cloud-init","options":{}}`), + }, + }, + { + name: "cloud-init-data", + fields: fields{ + Type: "org.osbuild.cloud-init", + Options: &CloudInitStageOptions{ + ConfigFiles: map[string]CloudInitConfigFile{ + "00-default_user.cfg": { + SystemInfo: &CloudInitConfigSystemInfo{ + DefaultUser: &CloudInitConfigDefaultUser{ + Name: "ec2-user", + }, + }, + }, + }, + }, + }, + args: args{ + data: []byte(`{"type":"org.osbuild.cloud-init","options":{"configuration_files":{"00-default_user.cfg":{"system_info":{"default_user":{"name":"ec2-user"}}}}}}`), + }, + }, { name: "chrony", fields: fields{