diff --git a/internal/osbuild2/chrony_stage.go b/internal/osbuild2/chrony_stage.go index 4233d02e5..1e3e4835c 100644 --- a/internal/osbuild2/chrony_stage.go +++ b/internal/osbuild2/chrony_stage.go @@ -1,11 +1,41 @@ package osbuild2 +import ( + "encoding/json" + "fmt" +) + +// Exactly one of 'Timeservers' or 'Servers' must be specified type ChronyStageOptions struct { - Timeservers []string `json:"timeservers"` + Timeservers []string `json:"timeservers,omitempty"` + Servers []ChronyConfigServer `json:"servers,omitempty"` } func (ChronyStageOptions) isStageOptions() {} +// Use '*ToPtr()' functions from the internal/common package to set the pointer values in literals +type ChronyConfigServer struct { + Hostname string `json:"hostname"` + Minpoll *int `json:"minpoll,omitempty"` + Maxpoll *int `json:"maxpoll,omitempty"` + Iburst *bool `json:"iburst,omitempty"` + Prefer *bool `json:"prefer,omitempty"` +} + +// Unexported struct for use in ChronyStageOptions's MarshalJSON() to prevent recursion +type chronyStageOptions struct { + Timeservers []string `json:"timeservers,omitempty"` + Servers []ChronyConfigServer `json:"servers,omitempty"` +} + +func (o ChronyStageOptions) MarshalJSON() ([]byte, error) { + if (len(o.Timeservers) != 0 && len(o.Servers) != 0) || (len(o.Timeservers) == 0 && len(o.Servers) == 0) { + return nil, fmt.Errorf("exactly one of 'Timeservers' or 'Servers' must be specified") + } + stageOptions := chronyStageOptions(o) + return json.Marshal(stageOptions) +} + func NewChronyStage(options *ChronyStageOptions) *Stage { return &Stage{ Type: "org.osbuild.chrony", diff --git a/internal/osbuild2/chrony_stage_test.go b/internal/osbuild2/chrony_stage_test.go index afb8ef8ce..2d8e390c0 100644 --- a/internal/osbuild2/chrony_stage_test.go +++ b/internal/osbuild2/chrony_stage_test.go @@ -1,6 +1,7 @@ package osbuild2 import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -14,3 +15,28 @@ func TestNewChronyStage(t *testing.T) { actualStage := NewChronyStage(&ChronyStageOptions{}) assert.Equal(t, expectedStage, actualStage) } + +func TestChronyStage_MarshalJSON_Invalid(t *testing.T) { + tests := []struct { + name string + options ChronyStageOptions + }{ + { + name: "not-timeservers-nor-servers", + options: ChronyStageOptions{}, + }, + { + name: "timeservers-and-servers", + options: ChronyStageOptions{ + Timeservers: []string{"ntp.example.com"}, + Servers: []ChronyConfigServer{{Hostname: "ntp2.example.com"}}, + }, + }, + } + for idx, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := json.Marshal(tt.options) + assert.NotNilf(t, err, "json.Marshal() didn't return an error [idx: %d]", idx) + }) + } +} diff --git a/internal/osbuild2/stage_test.go b/internal/osbuild2/stage_test.go index 86c4cb57b..6f5b073fa 100644 --- a/internal/osbuild2/stage_test.go +++ b/internal/osbuild2/stage_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/google/uuid" + "github.com/osbuild/osbuild-composer/internal/common" ) func TestStage_UnmarshalJSON(t *testing.T) { @@ -83,13 +84,41 @@ func TestStage_UnmarshalJSON(t *testing.T) { }, }, { - name: "chrony", + name: "chrony-timeservers", fields: fields{ - Type: "org.osbuild.chrony", - Options: &ChronyStageOptions{}, + Type: "org.osbuild.chrony", + Options: &ChronyStageOptions{ + Timeservers: []string{ + "ntp1.example.com", + "ntp2.example.com", + }, + }, }, args: args{ - data: []byte(`{"type":"org.osbuild.chrony","options":{"timeservers":null}}`), + data: []byte(`{"type":"org.osbuild.chrony","options":{"timeservers":["ntp1.example.com","ntp2.example.com"]}}`), + }, + }, + { + name: "chrony-servers", + fields: fields{ + Type: "org.osbuild.chrony", + Options: &ChronyStageOptions{ + Servers: []ChronyConfigServer{ + { + Hostname: "127.0.0.1", + Minpoll: common.IntToPtr(0), + Maxpoll: common.IntToPtr(4), + Iburst: common.BoolToPtr(true), + Prefer: common.BoolToPtr(false), + }, + { + Hostname: "ntp.example.com", + }, + }, + }, + }, + args: args{ + data: []byte(`{"type":"org.osbuild.chrony","options":{"servers":[{"hostname":"127.0.0.1","minpoll":0,"maxpoll":4,"iburst":true,"prefer":false},{"hostname":"ntp.example.com"}]}}`), }, }, {