diff --git a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go index 0a7d5f5eb..9d41faa7a 100644 --- a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go +++ b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go @@ -208,16 +208,19 @@ func (s *JobsManager) pollSchedules() []error { if err != nil { return []error{err} } - if activationData.Schedule != nil { + if activationData.Schedule != "" { var fire bool - fire, err = activationData.Schedule.ShouldFireNow() + fire, err = activationData.ShouldFireNow() if err != nil { return []error{err} } if fire { - activationData.Schedule = nil + activationData.Schedule = "" err = s.StateProvider.Delete(context, states.DeleteRequest{ ID: entry.ID, + Metadata: map[string]interface{}{ + "namespace": activationData.Namespace, + }, }) if err != nil { return []error{err} diff --git a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager_test.go b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager_test.go index b53a68b1c..4435cb0f8 100644 --- a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager_test.go @@ -180,7 +180,7 @@ func TestPoll(t *testing.T) { }) assert.Nil(t, err) jobManager.HandleScheduleEvent(context.Background(), v1alpha2.Event{ - Body: v1alpha2.ActivationData{Campaign: "campaign1", Activation: "activation1", Schedule: &v1alpha2.ScheduleSpec{Time: "03:04:05PM", Date: "2006-01-02"}}, + Body: v1alpha2.ActivationData{Campaign: "campaign1", Activation: "activation1", Schedule: "2006-01-02T15:04:05Z"}, }) enabled := jobManager.Enabled() assert.True(t, enabled) diff --git a/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go b/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go index 4f989483e..40e22193b 100644 --- a/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go +++ b/api/pkg/apis/v1alpha1/managers/stage/stage-manager.go @@ -324,7 +324,7 @@ func (s *StageManager) HandleDirectTriggerEvent(ctx context.Context, triggerData provider.(*remote.RemoteStageProvider).SetOutputsContext(triggerData.Outputs) } - if triggerData.Schedule != nil && !isRemote { + if triggerData.Schedule != "" && !isRemote { s.Context.Publish("schedule", v1alpha2.Event{ Body: triggerData, }) @@ -481,9 +481,8 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca inputs["__activationGeneration"] = triggerData.ActivationGeneration inputs["__previousStage"] = triggerData.TriggeringStage inputs["__site"] = s.VendorContext.SiteInfo.SiteId - if triggerData.Schedule != nil { - jSchedule, _ := json.Marshal(triggerData.Schedule) - inputs["__schedule"] = string(jSchedule) + if triggerData.Schedule != "" { + inputs["__schedule"] = triggerData.Schedule } for k, v := range inputs { var val interface{} @@ -573,7 +572,7 @@ func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.Ca provider.(*remote.RemoteStageProvider).SetOutputsContext(triggerData.Outputs) } - if triggerData.Schedule != nil { + if triggerData.Schedule != "" { s.Context.Publish("schedule", v1alpha2.Event{ Body: triggerData, }) diff --git a/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go b/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go index cb7c8ff6b..c4826538d 100644 --- a/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/stage/stage-manager_test.go @@ -1104,11 +1104,7 @@ func TestHandleDirectTriggerScheduleEvent(t *testing.T) { }, Outputs: nil, Provider: "providers.stage.counter", - Schedule: &v1alpha2.ScheduleSpec{ - Date: "2020-01-01", - Time: "12:00:00PM", - Zone: "PST", - }, + Schedule: "2020-01-01T12:00:00-08:00", } status := manager.HandleDirectTriggerEvent(context.Background(), activation) assert.Equal(t, v1alpha2.Paused, status.Status) @@ -1214,11 +1210,7 @@ func TestTriggerEventWithSchedule(t *testing.T) { }, Outputs: nil, Provider: "providers.stage.mock", - Schedule: &v1alpha2.ScheduleSpec{ - Date: "2020-01-01", - Time: "12:00:00PM", - Zone: "PST", - }, + Schedule: "2020-01-01T12:00:00-08:00", } status, _ := manager.HandleTriggerEvent(context.Background(), model.CampaignSpec{ diff --git a/api/pkg/apis/v1alpha1/model/campaign.go b/api/pkg/apis/v1alpha1/model/campaign.go index 80c4f685d..6572a5851 100644 --- a/api/pkg/apis/v1alpha1/model/campaign.go +++ b/api/pkg/apis/v1alpha1/model/campaign.go @@ -7,8 +7,11 @@ package model import ( + "encoding/json" "errors" + "fmt" "reflect" + "time" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" ) @@ -31,7 +34,42 @@ type StageSpec struct { StageSelector string `json:"stageSelector,omitempty"` Inputs map[string]interface{} `json:"inputs,omitempty"` HandleErrors bool `json:"handleErrors,omitempty"` - Schedule *v1alpha2.ScheduleSpec `json:"schedule,omitempty"` + Schedule string `json:"schedule,omitempty"` +} + +// UnmarshalJSON customizes the JSON unmarshalling for StageSpec +func (s *StageSpec) UnmarshalJSON(data []byte) error { + type Alias StageSpec + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + // validate if Schedule meet RFC 3339 + if s.Schedule != "" { + if _, err := time.Parse(time.RFC3339, s.Schedule); err != nil { + return fmt.Errorf("invalid timestamp format: %v", err) + } + } + return nil +} + +// MarshalJSON customizes the JSON marshalling for StageSpec +func (s StageSpec) MarshalJSON() ([]byte, error) { + type Alias StageSpec + if s.Schedule != "" { + if _, err := time.Parse(time.RFC3339, s.Schedule); err != nil { + return nil, fmt.Errorf("invalid timestamp format: %v", err) + } + } + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(&s), + }) } func (s StageSpec) DeepEquals(other IDeepEquals) (bool, error) { diff --git a/api/pkg/apis/v1alpha1/model/campaign_test.go b/api/pkg/apis/v1alpha1/model/campaign_test.go index 9e29dd099..986620e54 100644 --- a/api/pkg/apis/v1alpha1/model/campaign_test.go +++ b/api/pkg/apis/v1alpha1/model/campaign_test.go @@ -9,7 +9,6 @@ package model import ( "testing" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/stretchr/testify/assert" ) @@ -217,16 +216,8 @@ func TestStageNotMatch(t *testing.T) { "objectType": "sites", "namesObject": true, } - stage1.Schedule = &v1alpha2.ScheduleSpec{ - Date: "2020-10-31", - Time: "12:00:00PM", - Zone: "PDT", - } - stage2.Schedule = &v1alpha2.ScheduleSpec{ - Date: "2020-10-31", - Time: "12:00:00PM", - Zone: "PST", - } + stage1.Schedule = "2020-10-31T12:00:00-07:00" + stage2.Schedule = "2020-10-31T12:00:00-08:00" equal, err = stage1.DeepEquals(stage2) assert.Nil(t, err) assert.False(t, equal) diff --git a/api/pkg/apis/v1alpha1/vendors/stage-vendor.go b/api/pkg/apis/v1alpha1/vendors/stage-vendor.go index 92e517633..a9beab6dc 100644 --- a/api/pkg/apis/v1alpha1/vendors/stage-vendor.go +++ b/api/pkg/apis/v1alpha1/vendors/stage-vendor.go @@ -18,6 +18,7 @@ import ( "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/materialize" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/mock" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/wait" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" @@ -248,24 +249,21 @@ func (s *StageVendor) Init(config vendors.VendorConfig, factories []managers.IMa } // restore schedule - var schedule *v1alpha2.ScheduleSpec + var schedule = "" if v, ok := dataPackage.Inputs["__schedule"]; ok { - err = json.Unmarshal([]byte(v.(string)), &schedule) - if err != nil { - return err - } + schedule = utils.FormatAsString(v) } triggerData := v1alpha2.ActivationData{ - Activation: dataPackage.Inputs["__activation"].(string), - ActivationGeneration: dataPackage.Inputs["__activationGeneration"].(string), - Campaign: dataPackage.Inputs["__campaign"].(string), - Stage: dataPackage.Inputs["__stage"].(string), + Activation: utils.FormatAsString(dataPackage.Inputs["__activation"]), + ActivationGeneration: utils.FormatAsString(dataPackage.Inputs["__activationGeneration"]), + Campaign: utils.FormatAsString(dataPackage.Inputs["__campaign"]), + Stage: utils.FormatAsString(dataPackage.Inputs["__stage"]), Inputs: dataPackage.Inputs, Outputs: dataPackage.Outputs, Schedule: schedule, NeedsReport: true, - Namespace: dataPackage.Inputs["__namespace"].(string), + Namespace: utils.FormatAsString(dataPackage.Inputs["__namespace"]), } triggerData.Inputs["__origin"] = event.Metadata["origin"] diff --git a/coa/pkg/apis/v1alpha2/events.go b/coa/pkg/apis/v1alpha2/events.go index 6302e6212..ab8e5b257 100644 --- a/coa/pkg/apis/v1alpha2/events.go +++ b/coa/pkg/apis/v1alpha2/events.go @@ -8,6 +8,7 @@ package v1alpha2 import ( "encoding/json" + "fmt" "time" ) @@ -48,10 +49,45 @@ type ActivationData struct { Provider string `json:"provider,omitempty"` Config interface{} `json:"config,omitempty"` TriggeringStage string `json:"triggeringStage,omitempty"` - Schedule *ScheduleSpec `json:"schedule,omitempty"` + Schedule string `json:"schedule,omitempty"` NeedsReport bool `json:"needsReport,omitempty"` } +// UnmarshalJSON customizes the JSON unmarshalling for ActivationData +func (s *ActivationData) UnmarshalJSON(data []byte) error { + type Alias ActivationData + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + // validate if Schedule meet RFC 3339 + if s.Schedule != "" { + if _, err := time.Parse(time.RFC3339, s.Schedule); err != nil { + return fmt.Errorf("invalid timestamp format: %v", err) + } + } + return nil +} + +// MarshalJSON customizes the JSON marshalling for ActivationData +func (s ActivationData) MarshalJSON() ([]byte, error) { + type Alias ActivationData + if s.Schedule != "" { + if _, err := time.Parse(time.RFC3339, s.Schedule); err != nil { + return nil, fmt.Errorf("invalid timestamp format: %v", err) + } + } + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(&s), + }) +} + type HeartBeatAction string const ( @@ -65,14 +101,9 @@ type HeartBeatData struct { Action HeartBeatAction `json:"action"` Time time.Time `json:"time"` } -type ScheduleSpec struct { - Date string `json:"date"` - Time string `json:"time"` - Zone string `json:"zone"` -} -func (s ScheduleSpec) ShouldFireNow() (bool, error) { - dt, err := s.GetTime() +func (s ActivationData) ShouldFireNow() (bool, error) { + dt, err := time.Parse(time.RFC3339, s.Schedule) if err != nil { return false, err } @@ -80,42 +111,6 @@ func (s ScheduleSpec) ShouldFireNow() (bool, error) { dtUTC := dt.In(time.UTC) return dtUTC.Before(dtNow), nil } -func (s ScheduleSpec) GetTime() (time.Time, error) { - dt, err := parseTimeWithZone(s.Time, s.Date, s.Zone) - if err != nil { - return time.Time{}, err - } - return dt, nil -} - -func parseTimeWithZone(timeStr string, dateStr string, zoneStr string) (time.Time, error) { - dtStr := dateStr + " " + timeStr - - switch zoneStr { - case "LOCAL": - zoneStr = "" - case "PST", "PDT": - zoneStr = "America/Los_Angeles" - case "EST", "EDT": - zoneStr = "America/New_York" - case "CST", "CDT": - zoneStr = "America/Chicago" - case "MST", "MDT": - zoneStr = "America/Denver" - } - - loc, err := time.LoadLocation(zoneStr) - if err != nil { - return time.Time{}, err - } - - dt, err := time.ParseInLocation("2006-01-02 3:04:05PM", dtStr, loc) - if err != nil { - return time.Time{}, err - } - - return dt, nil -} type InputOutputData struct { Inputs map[string]interface{} `json:"inputs,omitempty"` diff --git a/coa/pkg/apis/v1alpha2/events_test.go b/coa/pkg/apis/v1alpha2/events_test.go index cef8bca4b..52dcd5626 100644 --- a/coa/pkg/apis/v1alpha2/events_test.go +++ b/coa/pkg/apis/v1alpha2/events_test.go @@ -8,156 +8,30 @@ package v1alpha2 import ( "testing" - "time" "github.com/stretchr/testify/assert" ) -func TestScheduleErrorTimeZone(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2020-01-01", - Time: "12:00:00PM", - Zone: "XXX", - } - _, err := schedule.GetTime() - assert.NotNil(t, err) -} - -func TestScheduleTimeZone(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2020-01-01", - Time: "12:00:00PM", - Zone: "PST", - } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2020-01-01 12:00:00 -0800 PST", dt.String()) -} - -func TestScheduleTimeZoneDaylight(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2020-10-31", - Time: "12:00:00PM", - Zone: "PDT", - } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2020-10-31 12:00:00 -0700 PDT", dt.String()) //This is parsed as PDT because it is daylight savings time -} - -func TestScheduleTimeZoneDaylight2(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2020-10-31", - Time: "12:00:00PM", - Zone: "PDT", - } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2020-10-31 12:00:00 -0700 PDT", dt.String()) -} - -func TestScheduleTimeZoneDaylight3(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2020-10-20", - Time: "11:53:03AM", - Zone: "PDT", - } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2020-10-20 11:53:03 -0700 PDT", dt.String()) -} - -func TestScheduleIANATimeZone(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2020-01-02", - Time: "12:00:00PM", - Zone: "America/Los_Angeles", - } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2020-01-02 12:00:00 -0800 PST", dt.String()) -} - -func TestScheduleEmpty(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2020-01-01", - Time: "12:00:00PM", - Zone: "", //this is equivalent to UTC - } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2020-01-01 12:00:00 +0000 UTC", dt.String()) -} - -func TestScheduleUTC(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2020-01-01", - Time: "12:00:00PM", - Zone: "UTC", - } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2020-01-01 12:00:00 +0000 UTC", dt.String()) -} - func TestScheduleShouldFire(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2023-10-17", - Time: "12:00:00PM", - Zone: "PDT", + activationData := ActivationData{ + Schedule: "2023-10-17T12:00:00-07:00", } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2023-10-17 12:00:00 -0700 PDT", dt.String()) - fire, _ := schedule.ShouldFireNow() + fire, _ := activationData.ShouldFireNow() assert.True(t, fire) } func TestScheduleShouldFireUTC(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2023-10-20", - Time: "9:48:00PM", - Zone: "UTC", - } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2023-10-20 21:48:00 +0000 UTC", dt.String()) - fire, _ := schedule.ShouldFireNow() - assert.True(t, fire) -} -func TestGetTime(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2023-10-20", - Time: "3:53:00PM", - Zone: "PDT", - } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2023-10-20 15:53:00 -0700 PDT", dt.String()) - assert.Equal(t, "2023-10-20 22:53:00 +0000 UTC", dt.In(time.UTC).String()) -} -func TestScheduleShouldFirePDT(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2023-10-20", - Time: "3:26:00PM", - Zone: "PDT", + activationData := ActivationData{ + Schedule: "2023-10-20T21:48:00Z", } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2023-10-20 15:26:00 -0700 PDT", dt.String()) - fire, _ := schedule.ShouldFireNow() + fire, _ := activationData.ShouldFireNow() assert.True(t, fire) } func TestScheduleShouldNotFire(t *testing.T) { - schedule := ScheduleSpec{ - Date: "2073-10-17", - Time: "12:00:00PM", - Zone: "PDT", + activationData := ActivationData{ + Schedule: "2073-10-17T12:00:00-07:00", } - dt, err := schedule.GetTime() - assert.Nil(t, err) - assert.Equal(t, "2073-10-17 12:00:00 -0700 PDT", dt.String()) - fire, _ := schedule.ShouldFireNow() + fire, _ := activationData.ShouldFireNow() assert.False(t, fire) // This should remain false for the next 50 years, so I guess we'll have to update this test in 2073 } diff --git a/docs/samples/campaigns/remote-schedule/campaign.yaml b/docs/samples/campaigns/remote-schedule/campaign.yaml index 946fbc046..50e572ea8 100644 --- a/docs/samples/campaigns/remote-schedule/campaign.yaml +++ b/docs/samples/campaigns/remote-schedule/campaign.yaml @@ -13,10 +13,7 @@ spec: contexts: "tokyo" inputs: operation: mock - schedule: - date: "2023-10-23" - time: "1:40:00PM" - zone: "America/Los_Angeles" + schedule: "2023-10-23T21:40:00-08:00" stage2: name: "stage2" provider: "providers.stage.remote" @@ -24,10 +21,7 @@ spec: contexts: "tokyo" inputs: operation: mock - schedule: - date: "2023-10-23" - time: "1:40:20PM" - zone: "America/Los_Angeles" + schedule: "2023-10-23T21:40:20-08:00" stage3: name: "stage3" provider: "providers.stage.remote" @@ -35,7 +29,4 @@ spec: contexts: "tokyo" inputs: operation: mock - schedule: - date: "2023-10-23" - time: "1:41:00PM" - zone: "America/Los_Angeles" \ No newline at end of file + schedule: "2023-10-23T21:41:00-08:00" \ No newline at end of file diff --git a/docs/samples/campaigns/scheduled/campaign.yaml b/docs/samples/campaigns/scheduled/campaign.yaml index b8b9ce120..643d5e51f 100644 --- a/docs/samples/campaigns/scheduled/campaign.yaml +++ b/docs/samples/campaigns/scheduled/campaign.yaml @@ -10,23 +10,14 @@ spec: name: "stage1" provider: "providers.stage.mock" stageSelector: "stage2" - schedule: - date: "2023-10-23" - time: "2:00:00PM" - zone: "America/Los_Angeles" + schedule: "2023-10-23T22:00:00-08:00" stage2: name: "stage2" provider: "providers.stage.mock" stageSelector: "stage3" - schedule: - date: "2023-10-23" - time: "2:01:00PM" - zone: "America/Los_Angeles" + schedule: "2023-10-23T22:01:00-08:00" stage3: name: "stage3" provider: "providers.stage.mock" stageSelector: "" - schedule: - date: "2023-10-23" - time: "2:02:00PM" - zone: "America/Los_Angeles" \ No newline at end of file + schedule: "2023-10-23T22:02:00-08:00" \ No newline at end of file diff --git a/docs/samples/piccolo/tiny_stack/campaign-schedule.yaml b/docs/samples/piccolo/tiny_stack/campaign-schedule.yaml index 6b12c65fd..f6907acef 100644 --- a/docs/samples/piccolo/tiny_stack/campaign-schedule.yaml +++ b/docs/samples/piccolo/tiny_stack/campaign-schedule.yaml @@ -34,10 +34,7 @@ spec: ebpf.event: "xdp" patchAction: add stageSelector: "remove" - schedule: - date: "2033-10-23" - time: "4:00:00PM" - zone: "America/Los_Angeles" + schedule: "2023-10-23T16:00:00-08:00" remove: name: "remove" provider: "providers.stage.patch" @@ -53,7 +50,4 @@ spec: name: ebpf-module patchAction: add stageSelector: "remove" - schedule: - date: "2033-10-23" - time: "4:00:00PM" - zone: "America/Los_Angeles" \ No newline at end of file + schedule: "2023-10-23T16:00:00-08:00" \ No newline at end of file diff --git a/k8s/apis/model/v1/common_types.go b/k8s/apis/model/v1/common_types.go index 427ef06c9..cb33b2b5d 100644 --- a/k8s/apis/model/v1/common_types.go +++ b/k8s/apis/model/v1/common_types.go @@ -7,7 +7,10 @@ package v1 import ( + "encoding/json" + "fmt" "strings" + "time" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -124,13 +127,6 @@ type SolutionSpec struct { type SolutionContainerSpec struct { } -// +kubebuilder:object:generate=true -type ScheduleSpec struct { - Date string `json:"date"` - Time string `json:"time"` - Zone string `json:"zone"` -} - // +kubebuilder:object:generate=true type StageSpec struct { Name string `json:"name,omitempty"` @@ -144,7 +140,42 @@ type StageSpec struct { // +kubebuilder:validation:Schemaless Inputs runtime.RawExtension `json:"inputs,omitempty"` TriggeringStage string `json:"triggeringStage,omitempty"` - Schedule *ScheduleSpec `json:"schedule,omitempty"` + Schedule string `json:"schedule,omitempty"` +} + +// UnmarshalJSON customizes the JSON unmarshalling for StageSpec +func (s *StageSpec) UnmarshalJSON(data []byte) error { + type Alias StageSpec + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + // validate if Schedule meet RFC 3339 + if s.Schedule != "" { + if _, err := time.Parse(time.RFC3339, s.Schedule); err != nil { + return fmt.Errorf("invalid timestamp format: %v", err) + } + } + return nil +} + +// MarshalJSON customizes the JSON marshalling for StageSpec +func (s StageSpec) MarshalJSON() ([]byte, error) { + type Alias StageSpec + if s.Schedule != "" { + if _, err := time.Parse(time.RFC3339, s.Schedule); err != nil { + return nil, fmt.Errorf("invalid timestamp format: %v", err) + } + } + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(&s), + }) } // +kubebuilder:object:generate=true diff --git a/k8s/apis/model/v1/common_types_test.go b/k8s/apis/model/v1/common_types_test.go new file mode 100644 index 000000000..23696a3e0 --- /dev/null +++ b/k8s/apis/model/v1/common_types_test.go @@ -0,0 +1,26 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package v1 + +import ( + "encoding/json" + "testing" +) + +func TestStageSpecSchedule(t *testing.T) { + jsonString := `{"schedule": "2021-01-30T08:30:10+08:00"}` + + var newStage StageSpec + var err = json.Unmarshal([]byte(jsonString), &newStage) + if err != nil { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } + targetTime := "2021-01-30T08:30:10+08:00" + if newStage.Schedule != targetTime { + t.Fatalf("Failed to unmarshal JSON: %v", err) + } +} diff --git a/k8s/apis/model/v1/zz_generated.deepcopy.go b/k8s/apis/model/v1/zz_generated.deepcopy.go index d721fff47..a882d3be3 100644 --- a/k8s/apis/model/v1/zz_generated.deepcopy.go +++ b/k8s/apis/model/v1/zz_generated.deepcopy.go @@ -279,21 +279,6 @@ func (in *ReconciliationPolicySpec) DeepCopy() *ReconciliationPolicySpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ScheduleSpec) DeepCopyInto(out *ScheduleSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduleSpec. -func (in *ScheduleSpec) DeepCopy() *ScheduleSpec { - if in == nil { - return nil - } - out := new(ScheduleSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SidecarSpec) DeepCopyInto(out *SidecarSpec) { *out = *in @@ -359,11 +344,6 @@ func (in *StageSpec) DeepCopyInto(out *StageSpec) { *out = *in in.Config.DeepCopyInto(&out.Config) in.Inputs.DeepCopyInto(&out.Inputs) - if in.Schedule != nil { - in, out := &in.Schedule, &out.Schedule - *out = new(ScheduleSpec) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StageSpec. diff --git a/k8s/config/oss/crd/bases/fabric.symphony_targetcontainers.yaml b/k8s/config/oss/crd/bases/fabric.symphony_targetcontainers.yaml index 818efaacb..2223f747a 100644 --- a/k8s/config/oss/crd/bases/fabric.symphony_targetcontainers.yaml +++ b/k8s/config/oss/crd/bases/fabric.symphony_targetcontainers.yaml @@ -18,7 +18,7 @@ spec: - name: v1 schema: openAPIV3Schema: - description: Target is the Schema for the targets API + description: TargetContainer is the Schema for the TargetContainers API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation diff --git a/k8s/config/oss/crd/bases/federation.symphony_catalogcontainers.yaml b/k8s/config/oss/crd/bases/federation.symphony_catalogcontainers.yaml index 9468b49fe..04a781a11 100644 --- a/k8s/config/oss/crd/bases/federation.symphony_catalogcontainers.yaml +++ b/k8s/config/oss/crd/bases/federation.symphony_catalogcontainers.yaml @@ -18,7 +18,7 @@ spec: - name: v1 schema: openAPIV3Schema: - description: CatalogContainer is the Schema for the catalogs API + description: CatalogContainer is the Schema for the CatalogContainers API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation diff --git a/k8s/config/oss/crd/bases/workflow.symphony_campaigncontainers.yaml b/k8s/config/oss/crd/bases/workflow.symphony_campaigncontainers.yaml index b0a24ab59..b0b13e63a 100644 --- a/k8s/config/oss/crd/bases/workflow.symphony_campaigncontainers.yaml +++ b/k8s/config/oss/crd/bases/workflow.symphony_campaigncontainers.yaml @@ -18,7 +18,7 @@ spec: - name: v1 schema: openAPIV3Schema: - description: CampaignContainer is the Schema for the campaigns API + description: CampaignContainer is the Schema for the CampaignContainers API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation diff --git a/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml b/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml index 77e87fb3f..938b69a44 100644 --- a/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml +++ b/k8s/config/oss/crd/bases/workflow.symphony_campaigns.yaml @@ -56,18 +56,7 @@ spec: provider: type: string schedule: - properties: - date: - type: string - time: - type: string - zone: - type: string - required: - - date - - time - - zone - type: object + type: string stageSelector: type: string triggeringStage: diff --git a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index b227968d4..63b1f1daa 100644 --- a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml @@ -110,7 +110,7 @@ spec: - name: v1 schema: openAPIV3Schema: - description: CampaignContainer is the Schema for the campaigns API + description: CampaignContainer is the Schema for the CampaignContainers API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -209,18 +209,7 @@ spec: provider: type: string schedule: - properties: - date: - type: string - time: - type: string - zone: - type: string - required: - - date - - time - - zone - type: object + type: string stageSelector: type: string triggeringStage: @@ -253,7 +242,7 @@ spec: - name: v1 schema: openAPIV3Schema: - description: CatalogContainer is the Schema for the catalogs API + description: CatalogContainer is the Schema for the CatalogContainers API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -1456,7 +1445,7 @@ spec: - name: v1 schema: openAPIV3Schema: - description: Target is the Schema for the targets API + description: TargetContainer is the Schema for the TargetContainers API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation diff --git a/test/integration/scenarios/04.workflow/manifest/campaign.yaml b/test/integration/scenarios/04.workflow/manifest/campaign.yaml index 2c679bcf3..c16d2450f 100644 --- a/test/integration/scenarios/04.workflow/manifest/campaign.yaml +++ b/test/integration/scenarios/04.workflow/manifest/campaign.yaml @@ -35,6 +35,7 @@ spec: name: deploy provider: providers.stage.materialize stageSelector: "" + schedule: "2020-10-31T12:00:00-07:00" config: baseUrl: http://symphony-service:8080/v1alpha2/ user: admin