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:
parent
fbf220707a
commit
d1029fae69
2 changed files with 278 additions and 35 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue