osbuild2: update cloud-init stage with new options

Related to: https://github.com/osbuild/osbuild/pull/866/

Introduce new fields and move structure validation into the constructor.
This will fail faster and hopefully provide less space for programming
errors. Another advantage is simplified code with less type aliases and
lines.
This commit is contained in:
Martin Sehnoutka 2021-11-19 11:41:25 +01:00 committed by Achilleas Koutsou
parent fbf220707a
commit d1029fae69
2 changed files with 278 additions and 35 deletions

View file

@ -1,7 +1,6 @@
package osbuild2
import (
"encoding/json"
"fmt"
)
@ -13,6 +12,9 @@ type CloudInitStageOptions struct {
func (CloudInitStageOptions) isStageOptions() {}
func NewCloudInitStage(options *CloudInitStageOptions) *Stage {
if err := options.Config.validate(); err != nil {
panic(err)
}
return &Stage{
Type: "org.osbuild.cloud-init",
Options: options,
@ -21,18 +23,11 @@ func NewCloudInitStage(options *CloudInitStageOptions) *Stage {
// Represents a cloud-init configuration file
type CloudInitConfigFile struct {
SystemInfo *CloudInitConfigSystemInfo `json:"system_info,omitempty"`
}
// Unexported alias for use in CloudInitConfigFile's MarshalJSON() to prevent recursion
type cloudInitConfigFile CloudInitConfigFile
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)
SystemInfo *CloudInitConfigSystemInfo `json:"system_info,omitempty"`
Reporting *CloudInitConfigReporting `json:"reporting,omitempty"`
Datasource *CloudInitConfigDatasource `json:"datasource,omitempty"`
DatasourceList []string `json:"datasource_list,omitempty"`
Output *CloudInitConfigOutput `json:"output,omitempty"`
}
// Represents the 'system_info' configuration section
@ -40,15 +35,31 @@ type CloudInitConfigSystemInfo struct {
DefaultUser *CloudInitConfigDefaultUser `json:"default_user,omitempty"`
}
// Unexported alias for use in CloudInitConfigSystemInfo's MarshalJSON() to prevent recursion
type cloudInitConfigSystemInfo CloudInitConfigSystemInfo
// Represents the 'reporting' configuration section
type CloudInitConfigReporting struct {
Logging *CloudInitConfigReportingHandlers `json:"logging,omitempty"`
Telemetry *CloudInitConfigReportingHandlers `json:"telemetry,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)
type CloudInitConfigReportingHandlers struct {
Type string `json:"type"`
}
// Represents the 'datasource' configuration section
type CloudInitConfigDatasource struct {
Azure *CloudInitConfigDatasourceAzure `json:"Azure,omitempty"`
}
type CloudInitConfigDatasourceAzure struct {
ApplyNetworkConfig bool `json:"apply_network_config"`
}
// Represents the 'output' configuration section
type CloudInitConfigOutput struct {
Init *string `json:"init,omitempty"`
Config *string `json:"config,omitempty"`
Final *string `json:"final,omitempty"`
All *string `json:"all,omitempty"`
}
// Configuration of the 'default' user created by cloud-init.
@ -56,13 +67,92 @@ type CloudInitConfigDefaultUser struct {
Name string `json:"name,omitempty"`
}
// Unexported alias for use in CloudInitConfigDefaultUser's MarshalJSON() to prevent recursion
type cloudInitConfigDefaultUser CloudInitConfigDefaultUser
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")
func (c CloudInitConfigFile) validate() error {
if c.SystemInfo == nil && c.Reporting == nil && c.Datasource == nil && len(c.DatasourceList) == 0 && c.Output == nil {
return fmt.Errorf("at least one cloud-init configuration option must be specified")
}
defaultUser := cloudInitConfigDefaultUser(du)
return json.Marshal(defaultUser)
if c.SystemInfo != nil {
if err := c.SystemInfo.validate(); err != nil {
return err
}
}
if c.Reporting != nil {
if err := c.Reporting.validate(); err != nil {
return err
}
}
if c.Datasource != nil {
if err := c.Datasource.validate(); err != nil {
return err
}
}
if len(c.DatasourceList) > 0 {
for _, d := range c.DatasourceList {
if d != "Azure" {
return fmt.Errorf("only 'Azure' is allowed as an item in the datasource_list")
}
}
}
if c.Output != nil {
if err := c.Output.validate(); err != nil {
return err
}
}
return nil
}
func (si CloudInitConfigSystemInfo) validate() error {
if si.DefaultUser == nil {
return fmt.Errorf("at least one configuration option must be specified for 'system_info' section")
} else {
return si.DefaultUser.validate()
}
}
func (r CloudInitConfigReporting) validate() error {
if r.Logging == nil && r.Telemetry == nil {
return fmt.Errorf("at least one configuration option must be specified for 'reporting' section")
}
if r.Logging != nil {
if err := r.Logging.validate(); err != nil {
return err
}
}
if r.Telemetry != nil {
if err := r.Telemetry.validate(); err != nil {
return err
}
}
return nil
}
func (r CloudInitConfigReportingHandlers) validate() error {
allowed_values := []string{"log", "print", "webhook", "hyperv"}
for _, v := range allowed_values {
if v == r.Type {
return nil
}
}
return fmt.Errorf("reporting parameters must be one of 'log', 'print', 'webhook', 'hyperv'")
}
func (d CloudInitConfigDatasource) validate() error {
if d.Azure == nil {
return fmt.Errorf("at least one configuration option must be specified for 'datasource' section")
}
return nil
}
func (o CloudInitConfigOutput) validate() error {
if o.Init == nil && o.Config == nil && o.Final == nil && o.All == nil {
return fmt.Errorf("at least one configuration option must be specified for 'output' section")
}
return nil
}
func (du CloudInitConfigDefaultUser) validate() error {
if du.Name == "" {
return fmt.Errorf("at least one configuration option must be specified for 'default_user' section")
}
return nil
}

View file

@ -2,21 +2,41 @@ package osbuild2
import (
"encoding/json"
"reflect"
"testing"
"github.com/osbuild/osbuild-composer/internal/common"
"github.com/stretchr/testify/assert"
)
func TestNewCloudInitStage(t *testing.T) {
expectedStage := &Stage{
Type: "org.osbuild.cloud-init",
Options: &CloudInitStageOptions{},
Type: "org.osbuild.cloud-init",
Options: &CloudInitStageOptions{
Filename: "aaa",
Config: CloudInitConfigFile{
SystemInfo: &CloudInitConfigSystemInfo{
DefaultUser: &CloudInitConfigDefaultUser{
Name: "foo",
},
},
},
},
}
actualStage := NewCloudInitStage(&CloudInitStageOptions{})
actualStage := NewCloudInitStage(&CloudInitStageOptions{
Filename: "aaa",
Config: CloudInitConfigFile{
SystemInfo: &CloudInitConfigSystemInfo{
DefaultUser: &CloudInitConfigDefaultUser{
Name: "foo",
},
},
},
})
assert.Equal(t, expectedStage, actualStage)
}
func TestCloudInitStage_MarshalJSON_Invalid(t *testing.T) {
func TestCloudInitStage_NewStage_Invalid(t *testing.T) {
tests := []struct {
name string
options CloudInitStageOptions
@ -55,8 +75,141 @@ func TestCloudInitStage_MarshalJSON_Invalid(t *testing.T) {
}
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)
assert.Panics(t, func() { NewCloudInitStage(&tt.options) }, "NewCloudInitStage didn't panic, but it should [idx: %d]", idx)
})
}
}
func TestCloudInitStage_MarshalJSON(t *testing.T) {
tests := []struct {
name string
options CloudInitStageOptions
json string
}{
{
name: "simple-cloud-init-config-with-system-info",
options: CloudInitStageOptions{
Config: CloudInitConfigFile{
SystemInfo: &CloudInitConfigSystemInfo{
DefaultUser: &CloudInitConfigDefaultUser{
Name: "foo",
},
},
},
},
json: `{"filename":"","config":{"system_info":{"default_user":{"name":"foo"}}}}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotBytes, err := json.Marshal(tt.options)
assert.NoError(t, err)
assert.Equal(t, tt.json, string(gotBytes))
})
}
}
func TestCloudInitStage_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
options CloudInitStageOptions
json string
}{
{
name: "simple-cloud-init-config-with-system-info",
options: CloudInitStageOptions{
Config: CloudInitConfigFile{
SystemInfo: &CloudInitConfigSystemInfo{
DefaultUser: &CloudInitConfigDefaultUser{
Name: "foo",
},
},
},
},
json: `{"filename":"","config":{"system_info":{"default_user":{"name":"foo"}}}}`,
},
{
name: "osbuild-test-suite-1",
options: CloudInitStageOptions{
Filename: "10-azure-kfp.cfg",
Config: CloudInitConfigFile{
Reporting: &CloudInitConfigReporting{
Logging: &CloudInitConfigReportingHandlers{
Type: "log",
},
Telemetry: &CloudInitConfigReportingHandlers{
Type: "hyperv",
},
},
},
},
json: `{
"filename": "10-azure-kfp.cfg",
"config": {
"reporting": {
"logging": {
"type": "log"
},
"telemetry": {
"type": "hyperv"
}
}
}
}`,
},
{
name: "osbuild-test-suite-2",
options: CloudInitStageOptions{
Filename: "91-azure_datasource.cfg",
Config: CloudInitConfigFile{
DatasourceList: []string{"Azure"},
Datasource: &CloudInitConfigDatasource{
Azure: &CloudInitConfigDatasourceAzure{
ApplyNetworkConfig: false,
},
},
},
},
json: `{
"filename": "91-azure_datasource.cfg",
"config": {
"datasource_list": [
"Azure"
],
"datasource": {
"Azure": {
"apply_network_config": false
}
}
}
}`,
},
{
name: "osbuild-test-suite-3",
options: CloudInitStageOptions{
Filename: "06_logging_override.cfg",
Config: CloudInitConfigFile{
Output: &CloudInitConfigOutput{
All: common.StringToPtr(">> /var/log/cloud-init-all.log"),
},
},
},
json: `{
"filename": "06_logging_override.cfg",
"config": {
"output": {
"all": ">> /var/log/cloud-init-all.log"
}
}
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var gotOptions CloudInitStageOptions
err := json.Unmarshal([]byte(tt.json), &gotOptions)
assert.NoError(t, err)
assert.True(t, reflect.DeepEqual(tt.options, gotOptions))
})
}
}