From e6934004dd001748303df259ce00b3307ac736e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yunus=20Sand=C4=B1kc=C4=B1?= Date: Sun, 11 Aug 2024 23:19:22 +0300 Subject: [PATCH] Recurring Scheduled Resource Creation Support (#80) --- .golangci.yaml | 4 +- cmd/manager/main.go | 5 +- ...loud.namecheap.com_scheduledresources.yaml | 28 ++- devops.sh | 6 +- docs/api.md | 14 +- examples/combination.yaml | 18 -- examples/exact-duration-combination.yaml | 18 -- ...piration.yaml => expiration-duration.yaml} | 4 +- ...ration.yaml => expiration-exact-date.yaml} | 2 +- examples/scheduled-resource-cronjob.yaml | 14 ++ ....yaml => scheduled-resource-duration.yaml} | 8 +- examples/scheduled-resource-exact-date.yaml | 14 ++ ...-resource-with-expiration-combination.yaml | 16 ++ .../mayfly/pkg/common/mock_Scheduler.go | 124 +++++++++++-- .../pkg/cache/mock_Cache.go | 2 +- .../pkg/client/mock_Client.go | 2 +- .../pkg/client/mock_SubResourceClient.go | 2 +- .../pkg/manager/mock_Manager.go | 2 +- .../groupversion_info.go | 6 +- .../scheduled_resource_types.go | 22 ++- .../zz_generated.deepcopy.go | 2 +- pkg/common/config.go | 2 +- pkg/common/scheduler.go | 39 ++++- pkg/common/scheduler_test.go | 19 +- pkg/common/utils.go | 2 +- pkg/common/utils_test.go | 4 +- pkg/controllers/expiration/controller.go | 12 +- pkg/controllers/expiration/controller_test.go | 17 +- .../scheduledresource/controller.go | 59 ++++--- .../scheduledresource/controller_test.go | 165 +++++++++++++----- 30 files changed, 452 insertions(+), 180 deletions(-) delete mode 100644 examples/combination.yaml delete mode 100644 examples/exact-duration-combination.yaml rename examples/{expiration.yaml => expiration-duration.yaml} (60%) rename examples/{exact-expiration.yaml => expiration-exact-date.yaml} (84%) create mode 100644 examples/scheduled-resource-cronjob.yaml rename examples/{scheduled_resource.yaml => scheduled-resource-duration.yaml} (51%) create mode 100644 examples/scheduled-resource-exact-date.yaml create mode 100644 examples/scheduled-resource-with-expiration-combination.yaml rename pkg/apis/{v1alpha1 => v1alpha2}/groupversion_info.go (82%) rename pkg/apis/{v1alpha1 => v1alpha2}/scheduled_resource_types.go (69%) rename pkg/apis/{v1alpha1 => v1alpha2}/zz_generated.deepcopy.go (99%) diff --git a/.golangci.yaml b/.golangci.yaml index 0f23549..a458f1b 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -2,7 +2,6 @@ linters: enable-all: true disable: - exhaustruct # Disallows to left unused fields in structs - - exhaustivestruct # Disallows to left unused fields in structs - wrapcheck # Disallows to use non-wrapped errors - gochecknoinits # Disallows to use init functions - ireturn # Disallows to return Interfaces @@ -22,5 +21,6 @@ run: go: '1.22' timeout: 10m tests: false - skip-dirs: +issues: + exclude-dirs: - cmd/benchmark diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 3dd90c5..7263867 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -3,10 +3,11 @@ package main import ( "fmt" + "github.com/NCCloud/mayfly/pkg/apis/v1alpha2" + "github.com/NCCloud/mayfly/pkg/controllers/expiration" "github.com/NCCloud/mayfly/pkg/controllers/scheduledresource" - "github.com/NCCloud/mayfly/pkg/apis/v1alpha1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "github.com/NCCloud/mayfly/pkg/common" @@ -29,7 +30,7 @@ func main() { scheme := runtime.NewScheme() config := common.NewConfig() - utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(v1alpha2.AddToScheme(scheme)) ctrl.SetLogger(logger) logger.Info("Configuration", "config", config) diff --git a/deploy/crds/cloud.namecheap.com_scheduledresources.yaml b/deploy/crds/cloud.namecheap.com_scheduledresources.yaml index 2bdbcf7..c284aec 100644 --- a/deploy/crds/cloud.namecheap.com_scheduledresources.yaml +++ b/deploy/crds/cloud.namecheap.com_scheduledresources.yaml @@ -15,16 +15,22 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: - - jsonPath: .spec.in - name: In + - jsonPath: .spec.schedule + name: Schedule + type: string + - jsonPath: .status.nextRun + name: Next Run + type: string + - jsonPath: .status.lastRun + name: Last Run type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - jsonPath: .status.condition name: Condition type: string - name: v1alpha1 + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 schema: openAPIV3Schema: properties: @@ -49,18 +55,24 @@ spec: properties: content: type: string - in: + schedule: type: string required: - content - - in + - schedule type: object status: properties: condition: type: string + lastRun: + type: string + nextRun: + type: string required: - condition + - lastRun + - nextRun type: object type: object served: true diff --git a/devops.sh b/devops.sh index 0d8831a..9824cd3 100755 --- a/devops.sh +++ b/devops.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash export CONTROLLER_GEN_VERSION="v0.15.0" -export GOLANGCI_LINT_VERSION="v1.58.1" -export MOCKERY_GEN_VERSION="v2.42.3" +export GOLANGCI_LINT_VERSION="v1.59.1" +export MOCKERY_GEN_VERSION="v2.44.1" export GOFUMPT_VERSION="v0.6.0" export TESTENV_VERSION="1.25.x!" @@ -36,8 +36,6 @@ generate() { rm -rf deploy/crds controller-gen object paths="./..." controller-gen crd paths="./..." output:dir=deploy/crds - sed '/Compiled/d' pkg/apis/v1alpha1/zz_generated.deepcopy.go > pkg/apis/v1alpha1/zz_generated.deepcopy.gotmp - mv pkg/apis/v1alpha1/zz_generated.deepcopy.gotmp pkg/apis/v1alpha1/zz_generated.deepcopy.go crd-ref-docs --source-path=./pkg/apis --config .apidoc.yaml --renderer markdown --output-path=./docs/api.md mockery } diff --git a/docs/api.md b/docs/api.md index 68c0af5..c0d88ab 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,12 +1,12 @@ # API Reference ## Packages -- [cloud.namecheap.com/v1alpha1](#cloudnamecheapcomv1alpha1) +- [cloud.namecheap.com/v1alpha2](#cloudnamecheapcomv1alpha2) -## cloud.namecheap.com/v1alpha1 +## cloud.namecheap.com/v1alpha2 -Package v1alpha1 contains API Schema definitions for the v1alpha1 API group +Package v1alpha2 contains API Schema definitions for the v1alpha2 API group ### Resource Types - [ScheduledResource](#scheduledresource) @@ -26,7 +26,7 @@ _Appears in:_ | Field | Description | | --- | --- | -| `Created` | | +| `Finished` | | | `Scheduled` | | | `Failed` | | @@ -45,7 +45,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `apiVersion` _string_ | `cloud.namecheap.com/v1alpha1` | | | +| `apiVersion` _string_ | `cloud.namecheap.com/v1alpha2` | | | | `kind` _string_ | `ScheduledResource` | | | | `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | | `spec` _[ScheduledResourceSpec](#scheduledresourcespec)_ | | | | @@ -65,7 +65,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `in` _string_ | | | | +| `schedule` _string_ | | | | | `content` _string_ | | | | @@ -82,6 +82,8 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | +| `nextRun` _string_ | | | | +| `lastRun` _string_ | | | | | `condition` _[Condition](#condition)_ | | | | diff --git a/examples/combination.yaml b/examples/combination.yaml deleted file mode 100644 index da66c61..0000000 --- a/examples/combination.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: cloud.namecheap.com/v1alpha1 -kind: ScheduledResource -metadata: - name: combination-example - annotations: - mayfly.cloud.namecheap.com/expire: 10s -spec: - in: "5s" - content: | - apiVersion: v1 - kind: Secret - metadata: - name: combination-example - namespace: default - annotations: - mayfly.cloud.namecheap.com/expire: 10s - data: - .secret-file: dmFsdWUtMg0KDQo= \ No newline at end of file diff --git a/examples/exact-duration-combination.yaml b/examples/exact-duration-combination.yaml deleted file mode 100644 index 2411b17..0000000 --- a/examples/exact-duration-combination.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: cloud.namecheap.com/v1alpha1 -kind: ScheduledResource -metadata: - name: combination-example - annotations: - mayfly.cloud.namecheap.com/expire: "2024-12-31T00:00:00Z" -spec: - in: "10s" - content: | - apiVersion: v1 - kind: Secret - metadata: - name: combination-example - namespace: default - annotations: - mayfly.cloud.namecheap.com/expire: "2024-12-31 22:05:00" - data: - .secret-file: dmFsdWUtMg0KDQo= \ No newline at end of file diff --git a/examples/expiration.yaml b/examples/expiration-duration.yaml similarity index 60% rename from examples/expiration.yaml rename to examples/expiration-duration.yaml index 2fb2c74..720a7d8 100644 --- a/examples/expiration.yaml +++ b/examples/expiration-duration.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Secret metadata: - name: expiration-example + name: expiration-duration namespace: default annotations: - mayfly.cloud.namecheap.com/expire: "50s" + mayfly.cloud.namecheap.com/expire: "10s" data: .secret-file: dmFsdWUtMg0KDQo= \ No newline at end of file diff --git a/examples/exact-expiration.yaml b/examples/expiration-exact-date.yaml similarity index 84% rename from examples/exact-expiration.yaml rename to examples/expiration-exact-date.yaml index 5a69372..3b13e74 100644 --- a/examples/exact-expiration.yaml +++ b/examples/expiration-exact-date.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Secret metadata: - name: exact-expiration-example + name: expiration-exact-date namespace: default annotations: mayfly.cloud.namecheap.com/expire: "2024-12-31T00:00:00Z" diff --git a/examples/scheduled-resource-cronjob.yaml b/examples/scheduled-resource-cronjob.yaml new file mode 100644 index 0000000..ba5aeb3 --- /dev/null +++ b/examples/scheduled-resource-cronjob.yaml @@ -0,0 +1,14 @@ +apiVersion: cloud.namecheap.com/v1alpha2 +kind: ScheduledResource +metadata: + name: scheduled-resource-cronjob +spec: + schedule: "*/10 * * * * *" # Creates every 10 second + content: | + apiVersion: v1 + kind: Secret + metadata: + name: scheduled-resource-cronjob-example + namespace: default + data: + .secret-file: dmFsdWUtMg0KDQo= \ No newline at end of file diff --git a/examples/scheduled_resource.yaml b/examples/scheduled-resource-duration.yaml similarity index 51% rename from examples/scheduled_resource.yaml rename to examples/scheduled-resource-duration.yaml index ce5e72b..6c0f1a8 100644 --- a/examples/scheduled_resource.yaml +++ b/examples/scheduled-resource-duration.yaml @@ -1,14 +1,14 @@ -apiVersion: cloud.namecheap.com/v1alpha1 +apiVersion: cloud.namecheap.com/v1alpha2 kind: ScheduledResource metadata: - name: scheduled-resource-example + name: scheduled-resource-duration spec: - in: "5s" + schedule: "10s" # Creates in 10 seconds content: | apiVersion: v1 kind: Secret metadata: - name: scheduled-resource-example + name: scheduled-resource-duration-example namespace: default data: .secret-file: dmFsdWUtMg0KDQo= \ No newline at end of file diff --git a/examples/scheduled-resource-exact-date.yaml b/examples/scheduled-resource-exact-date.yaml new file mode 100644 index 0000000..d5366ff --- /dev/null +++ b/examples/scheduled-resource-exact-date.yaml @@ -0,0 +1,14 @@ +apiVersion: cloud.namecheap.com/v1alpha2 +kind: ScheduledResource +metadata: + name: scheduled-resource-exact-date +spec: + schedule: "2024-12-31T00:00:00Z" # Creates at exact date + content: | + apiVersion: v1 + kind: Secret + metadata: + name: scheduled-resource-exact-date-example + namespace: default + data: + .secret-file: dmFsdWUtMg0KDQo= \ No newline at end of file diff --git a/examples/scheduled-resource-with-expiration-combination.yaml b/examples/scheduled-resource-with-expiration-combination.yaml new file mode 100644 index 0000000..009fcd1 --- /dev/null +++ b/examples/scheduled-resource-with-expiration-combination.yaml @@ -0,0 +1,16 @@ +apiVersion: cloud.namecheap.com/v1alpha2 +kind: ScheduledResource +metadata: + name: scheduled-resource-with-expiration-combination +spec: + schedule: "*/20 * * * * *" # Creates every 20 seconds + content: | + apiVersion: v1 + kind: Secret + metadata: + generateName: scheduled-resource-with-expiration-combination-example- + namespace: default + annotations: + mayfly.cloud.namecheap.com/expire: "10s" # Deletes in 10 seconds after created + data: + .secret-file: dmFsdWUtMg0KDQo= \ No newline at end of file diff --git a/mocks/github.com/NCCloud/mayfly/pkg/common/mock_Scheduler.go b/mocks/github.com/NCCloud/mayfly/pkg/common/mock_Scheduler.go index 30036d4..1d7e929 100644 --- a/mocks/github.com/NCCloud/mayfly/pkg/common/mock_Scheduler.go +++ b/mocks/github.com/NCCloud/mayfly/pkg/common/mock_Scheduler.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.3. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package common @@ -21,17 +21,17 @@ func (_m *MockScheduler) EXPECT() *MockScheduler_Expecter { return &MockScheduler_Expecter{mock: &_m.Mock} } -// CreateOrUpdateTask provides a mock function with given fields: tag, date, task -func (_m *MockScheduler) CreateOrUpdateTask(tag string, date time.Time, task func() error) error { - ret := _m.Called(tag, date, task) +// CreateOrUpdateOneTimeTask provides a mock function with given fields: tag, at, task +func (_m *MockScheduler) CreateOrUpdateOneTimeTask(tag string, at time.Time, task func() error) error { + ret := _m.Called(tag, at, task) if len(ret) == 0 { - panic("no return value specified for CreateOrUpdateTask") + panic("no return value specified for CreateOrUpdateOneTimeTask") } var r0 error if rf, ok := ret.Get(0).(func(string, time.Time, func() error) error); ok { - r0 = rf(tag, date, task) + r0 = rf(tag, at, task) } else { r0 = ret.Error(0) } @@ -39,32 +39,80 @@ func (_m *MockScheduler) CreateOrUpdateTask(tag string, date time.Time, task fun return r0 } -// MockScheduler_CreateOrUpdateTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateOrUpdateTask' -type MockScheduler_CreateOrUpdateTask_Call struct { +// MockScheduler_CreateOrUpdateOneTimeTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateOrUpdateOneTimeTask' +type MockScheduler_CreateOrUpdateOneTimeTask_Call struct { *mock.Call } -// CreateOrUpdateTask is a helper method to define mock.On call +// CreateOrUpdateOneTimeTask is a helper method to define mock.On call // - tag string -// - date time.Time +// - at time.Time // - task func() error -func (_e *MockScheduler_Expecter) CreateOrUpdateTask(tag interface{}, date interface{}, task interface{}) *MockScheduler_CreateOrUpdateTask_Call { - return &MockScheduler_CreateOrUpdateTask_Call{Call: _e.mock.On("CreateOrUpdateTask", tag, date, task)} +func (_e *MockScheduler_Expecter) CreateOrUpdateOneTimeTask(tag interface{}, at interface{}, task interface{}) *MockScheduler_CreateOrUpdateOneTimeTask_Call { + return &MockScheduler_CreateOrUpdateOneTimeTask_Call{Call: _e.mock.On("CreateOrUpdateOneTimeTask", tag, at, task)} } -func (_c *MockScheduler_CreateOrUpdateTask_Call) Run(run func(tag string, date time.Time, task func() error)) *MockScheduler_CreateOrUpdateTask_Call { +func (_c *MockScheduler_CreateOrUpdateOneTimeTask_Call) Run(run func(tag string, at time.Time, task func() error)) *MockScheduler_CreateOrUpdateOneTimeTask_Call { _c.Call.Run(func(args mock.Arguments) { run(args[0].(string), args[1].(time.Time), args[2].(func() error)) }) return _c } -func (_c *MockScheduler_CreateOrUpdateTask_Call) Return(_a0 error) *MockScheduler_CreateOrUpdateTask_Call { +func (_c *MockScheduler_CreateOrUpdateOneTimeTask_Call) Return(_a0 error) *MockScheduler_CreateOrUpdateOneTimeTask_Call { _c.Call.Return(_a0) return _c } -func (_c *MockScheduler_CreateOrUpdateTask_Call) RunAndReturn(run func(string, time.Time, func() error) error) *MockScheduler_CreateOrUpdateTask_Call { +func (_c *MockScheduler_CreateOrUpdateOneTimeTask_Call) RunAndReturn(run func(string, time.Time, func() error) error) *MockScheduler_CreateOrUpdateOneTimeTask_Call { + _c.Call.Return(run) + return _c +} + +// CreateOrUpdateRecurringTask provides a mock function with given fields: tag, cron, task +func (_m *MockScheduler) CreateOrUpdateRecurringTask(tag string, cron string, task func() error) error { + ret := _m.Called(tag, cron, task) + + if len(ret) == 0 { + panic("no return value specified for CreateOrUpdateRecurringTask") + } + + var r0 error + if rf, ok := ret.Get(0).(func(string, string, func() error) error); ok { + r0 = rf(tag, cron, task) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockScheduler_CreateOrUpdateRecurringTask_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateOrUpdateRecurringTask' +type MockScheduler_CreateOrUpdateRecurringTask_Call struct { + *mock.Call +} + +// CreateOrUpdateRecurringTask is a helper method to define mock.On call +// - tag string +// - cron string +// - task func() error +func (_e *MockScheduler_Expecter) CreateOrUpdateRecurringTask(tag interface{}, cron interface{}, task interface{}) *MockScheduler_CreateOrUpdateRecurringTask_Call { + return &MockScheduler_CreateOrUpdateRecurringTask_Call{Call: _e.mock.On("CreateOrUpdateRecurringTask", tag, cron, task)} +} + +func (_c *MockScheduler_CreateOrUpdateRecurringTask_Call) Run(run func(tag string, cron string, task func() error)) *MockScheduler_CreateOrUpdateRecurringTask_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string), args[2].(func() error)) + }) + return _c +} + +func (_c *MockScheduler_CreateOrUpdateRecurringTask_Call) Return(_a0 error) *MockScheduler_CreateOrUpdateRecurringTask_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockScheduler_CreateOrUpdateRecurringTask_Call) RunAndReturn(run func(string, string, func() error) error) *MockScheduler_CreateOrUpdateRecurringTask_Call { _c.Call.Return(run) return _c } @@ -115,6 +163,52 @@ func (_c *MockScheduler_DeleteTask_Call) RunAndReturn(run func(string) error) *M return _c } +// GetTaskNextRun provides a mock function with given fields: tag +func (_m *MockScheduler) GetTaskNextRun(tag string) string { + ret := _m.Called(tag) + + if len(ret) == 0 { + panic("no return value specified for GetTaskNextRun") + } + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(tag) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockScheduler_GetTaskNextRun_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTaskNextRun' +type MockScheduler_GetTaskNextRun_Call struct { + *mock.Call +} + +// GetTaskNextRun is a helper method to define mock.On call +// - tag string +func (_e *MockScheduler_Expecter) GetTaskNextRun(tag interface{}) *MockScheduler_GetTaskNextRun_Call { + return &MockScheduler_GetTaskNextRun_Call{Call: _e.mock.On("GetTaskNextRun", tag)} +} + +func (_c *MockScheduler_GetTaskNextRun_Call) Run(run func(tag string)) *MockScheduler_GetTaskNextRun_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockScheduler_GetTaskNextRun_Call) Return(_a0 string) *MockScheduler_GetTaskNextRun_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockScheduler_GetTaskNextRun_Call) RunAndReturn(run func(string) string) *MockScheduler_GetTaskNextRun_Call { + _c.Call.Return(run) + return _c +} + // NewMockScheduler creates a new instance of MockScheduler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewMockScheduler(t interface { diff --git a/mocks/sigs.k8s.io/controller-runtime/pkg/cache/mock_Cache.go b/mocks/sigs.k8s.io/controller-runtime/pkg/cache/mock_Cache.go index 514673d..51472f4 100644 --- a/mocks/sigs.k8s.io/controller-runtime/pkg/cache/mock_Cache.go +++ b/mocks/sigs.k8s.io/controller-runtime/pkg/cache/mock_Cache.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.3. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package cache diff --git a/mocks/sigs.k8s.io/controller-runtime/pkg/client/mock_Client.go b/mocks/sigs.k8s.io/controller-runtime/pkg/client/mock_Client.go index 4d11a91..622867c 100644 --- a/mocks/sigs.k8s.io/controller-runtime/pkg/client/mock_Client.go +++ b/mocks/sigs.k8s.io/controller-runtime/pkg/client/mock_Client.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.3. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package client diff --git a/mocks/sigs.k8s.io/controller-runtime/pkg/client/mock_SubResourceClient.go b/mocks/sigs.k8s.io/controller-runtime/pkg/client/mock_SubResourceClient.go index cd59e2b..44e63f5 100644 --- a/mocks/sigs.k8s.io/controller-runtime/pkg/client/mock_SubResourceClient.go +++ b/mocks/sigs.k8s.io/controller-runtime/pkg/client/mock_SubResourceClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.3. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package client diff --git a/mocks/sigs.k8s.io/controller-runtime/pkg/manager/mock_Manager.go b/mocks/sigs.k8s.io/controller-runtime/pkg/manager/mock_Manager.go index 83f7e58..b489f9b 100644 --- a/mocks/sigs.k8s.io/controller-runtime/pkg/manager/mock_Manager.go +++ b/mocks/sigs.k8s.io/controller-runtime/pkg/manager/mock_Manager.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.3. DO NOT EDIT. +// Code generated by mockery v2.44.1. DO NOT EDIT. package manager diff --git a/pkg/apis/v1alpha1/groupversion_info.go b/pkg/apis/v1alpha2/groupversion_info.go similarity index 82% rename from pkg/apis/v1alpha1/groupversion_info.go rename to pkg/apis/v1alpha2/groupversion_info.go index d742ebb..6ba25c2 100644 --- a/pkg/apis/v1alpha1/groupversion_info.go +++ b/pkg/apis/v1alpha2/groupversion_info.go @@ -1,7 +1,7 @@ -// Package v1alpha1 contains API Schema definitions for the v1alpha1 API group +// Package v1alpha2 contains API Schema definitions for the v1alpha2 API group // +kubebuilder:object:generate=true // +groupName=cloud.namecheap.com -package v1alpha1 +package v1alpha2 import ( "k8s.io/apimachinery/pkg/runtime/schema" @@ -10,7 +10,7 @@ import ( var ( // GroupVersion is group version used to register these objects. - GroupVersion = schema.GroupVersion{Group: "cloud.namecheap.com", Version: "v1alpha1"} + GroupVersion = schema.GroupVersion{Group: "cloud.namecheap.com", Version: "v1alpha2"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} diff --git a/pkg/apis/v1alpha1/scheduled_resource_types.go b/pkg/apis/v1alpha2/scheduled_resource_types.go similarity index 69% rename from pkg/apis/v1alpha1/scheduled_resource_types.go rename to pkg/apis/v1alpha2/scheduled_resource_types.go index 9cc75b4..f4edc54 100644 --- a/pkg/apis/v1alpha1/scheduled_resource_types.go +++ b/pkg/apis/v1alpha2/scheduled_resource_types.go @@ -1,4 +1,4 @@ -package v1alpha1 +package v1alpha2 import ( "errors" @@ -14,7 +14,7 @@ type ( ) const ( - ConditionCreated Condition = "Created" + ConditionFinished Condition = "Finished" ConditionScheduled Condition = "Scheduled" ConditionFailed Condition = "Failed" ) @@ -25,11 +25,13 @@ func init() { SchemeBuilder.Register(&ScheduledResource{}, &ScheduledResourceList{}) } -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:printcolumn:name="In",type=string,JSONPath=".spec.in" -//+kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" -//+kubebuilder:printcolumn:name="Condition",type=string,JSONPath=".status.condition" +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Schedule",type=string,JSONPath=".spec.schedule" +// +kubebuilder:printcolumn:name="Next Run",type=string,JSONPath=".status.nextRun" +// +kubebuilder:printcolumn:name="Last Run",type=string,JSONPath=".status.lastRun" +// +kubebuilder:printcolumn:name="Condition",type=string,JSONPath=".status.condition" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" type ScheduledResource struct { metav1.TypeMeta `json:",inline"` @@ -48,11 +50,13 @@ type ScheduledResourceList struct { } type ScheduledResourceSpec struct { - In string `json:"in"` - Content string `json:"content"` + Schedule string `json:"schedule"` + Content string `json:"content"` } type ScheduledResourceStatus struct { + NextRun string `json:"nextRun"` + LastRun string `json:"lastRun"` Condition Condition `json:"condition"` } diff --git a/pkg/apis/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/v1alpha2/zz_generated.deepcopy.go similarity index 99% rename from pkg/apis/v1alpha1/zz_generated.deepcopy.go rename to pkg/apis/v1alpha2/zz_generated.deepcopy.go index 0e7fb1c..1e69b27 100644 --- a/pkg/apis/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/v1alpha2/zz_generated.deepcopy.go @@ -2,7 +2,7 @@ // Code generated by controller-gen. DO NOT EDIT. -package v1alpha1 +package v1alpha2 import ( runtime "k8s.io/apimachinery/pkg/runtime" diff --git a/pkg/common/config.go b/pkg/common/config.go index 0c3b6df..8e0693c 100644 --- a/pkg/common/config.go +++ b/pkg/common/config.go @@ -11,7 +11,7 @@ type Config struct { SyncPeriod time.Duration `env:"SYNC_PERIOD" envDefault:"60m"` MonitoringInterval time.Duration `env:"MONITORING_INTERVAL" envDefault:"5s"` ExpirationLabel string `env:"EXPIRATION_LABEL" envDefault:"mayfly.cloud.namecheap.com/expire"` - Resources []string `env:"RESOURCES" envSeparator:"," envDefault:"v1;Secret,cloud.namecheap.com/v1alpha1;ScheduledResource"` + Resources []string `env:"RESOURCES" envSeparator:"," envDefault:"v1;Secret,cloud.namecheap.com/v1alpha2;ScheduledResource"` } func NewConfig() *Config { diff --git a/pkg/common/scheduler.go b/pkg/common/scheduler.go index b54582f..c9d825c 100644 --- a/pkg/common/scheduler.go +++ b/pkg/common/scheduler.go @@ -9,8 +9,10 @@ import ( ) type Scheduler interface { - CreateOrUpdateTask(tag string, date time.Time, task func() error) error + CreateOrUpdateOneTimeTask(tag string, at time.Time, task func() error) error + CreateOrUpdateRecurringTask(tag string, cron string, task func() error) error DeleteTask(tag string) error + GetTaskNextRun(tag string) string } type scheduler struct { @@ -40,7 +42,7 @@ func NewScheduler(config *Config) Scheduler { return schedulerInstance } -func (s *scheduler) CreateOrUpdateTask(tag string, date time.Time, task func() error) error { +func (s *scheduler) CreateOrUpdateOneTimeTask(tag string, date time.Time, task func() error) error { job := pie.Of(s.scheduler.Jobs()).Filter(func(job gocron.Job) bool { return slices.Contains(job.Tags(), tag) }).First() @@ -58,6 +60,39 @@ func (s *scheduler) CreateOrUpdateTask(tag string, date time.Time, task func() e return jobErr } +func (s *scheduler) CreateOrUpdateRecurringTask(tag string, cron string, task func() error) error { + job := pie.Of(s.scheduler.Jobs()).Filter(func(job gocron.Job) bool { + return slices.Contains(job.Tags(), tag) + }).First() + + if job != nil { + _, updateErr := s.scheduler.Update(job.ID(), gocron.CronJob( + cron, true), gocron.NewTask(task), gocron.WithTags(tag)) + + return updateErr + } + + _, jobErr := s.scheduler.NewJob(gocron.CronJob( + cron, true), gocron.NewTask(task), gocron.WithTags(tag)) + + return jobErr +} + +func (s *scheduler) GetTaskNextRun(tag string) string { + job := pie.Of(s.scheduler.Jobs()).Filter(func(job gocron.Job) bool { + return slices.Contains(job.Tags(), tag) + }).First() + + if job != nil { + nextRun, nextRunErr := job.NextRun() + if nextRunErr == nil { + return nextRun.Format(time.RFC3339) + } + } + + return "" +} + func (s *scheduler) DeleteTask(tag string) error { job := pie.Of(s.scheduler.Jobs()).Filter(func(job gocron.Job) bool { return slices.Contains(job.Tags(), tag) diff --git a/pkg/common/scheduler_test.go b/pkg/common/scheduler_test.go index d440380..3804a36 100644 --- a/pkg/common/scheduler_test.go +++ b/pkg/common/scheduler_test.go @@ -20,14 +20,29 @@ func TestScheduler_New(t *testing.T) { assert.IsType(t, schedulerInstance, &scheduler{}) } -func TestScheduler_CreateOrUpdateTask(t *testing.T) { +func TestScheduler_CreateOrUpdateOneTimeTask(t *testing.T) { // given config := NewConfig() schedulerInstance := NewScheduler(config) // when createOrUpdateTaskErr := schedulerInstance. - CreateOrUpdateTask("monitoring", gofakeit.FutureDate().Add(1*time.Hour), func() error { + CreateOrUpdateOneTimeTask("monitoring", gofakeit.FutureDate().Add(1*time.Hour), func() error { + return nil + }) + + // then + assert.Nil(t, createOrUpdateTaskErr) +} + +func TestScheduler_CreateOrUpdateRecurringTask(t *testing.T) { + // given + config := NewConfig() + schedulerInstance := NewScheduler(config) + + // when + createOrUpdateTaskErr := schedulerInstance. + CreateOrUpdateRecurringTask("monitoring", "* * * * * *", func() error { return nil }) diff --git a/pkg/common/utils.go b/pkg/common/utils.go index 3b008dc..b1e07ce 100644 --- a/pkg/common/utils.go +++ b/pkg/common/utils.go @@ -12,7 +12,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -func ResolveSchedule(creationTimestamp metav1.Time, dateTimeDuration string) (time.Time, error) { +func ResolveOneTimeSchedule(creationTimestamp metav1.Time, dateTimeDuration string) (time.Time, error) { duration, parseDurationErr := time.ParseDuration(dateTimeDuration) if parseDurationErr == nil { return creationTimestamp.Add(duration), nil diff --git a/pkg/common/utils_test.go b/pkg/common/utils_test.go index 02ef2ff..fa98013 100644 --- a/pkg/common/utils_test.go +++ b/pkg/common/utils_test.go @@ -15,7 +15,7 @@ func TestResolveSchedule_Date(t *testing.T) { date := gofakeit.Date() // when - schedule, scheduleErr := ResolveSchedule(metav1.Time{}, date.String()) + schedule, scheduleErr := ResolveOneTimeSchedule(metav1.Time{}, date.String()) // then assert.Nil(t, scheduleErr) @@ -34,7 +34,7 @@ func TestResolveSchedule_Duration(t *testing.T) { expected := currentDate.Add(duration) // when - schedule, scheduleErr := ResolveSchedule(currentDate, duration.String()) + schedule, scheduleErr := ResolveOneTimeSchedule(currentDate, duration.String()) // then assert.Nil(t, scheduleErr) diff --git a/pkg/controllers/expiration/controller.go b/pkg/controllers/expiration/controller.go index 01422a8..2353971 100644 --- a/pkg/controllers/expiration/controller.go +++ b/pkg/controllers/expiration/controller.go @@ -48,17 +48,17 @@ func (r *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, client.IgnoreNotFound(getErr) } - annotation, hasAnnotation := resource.GetAnnotations()[r.config.ExpirationLabel] - if !hasAnnotation { + expiration, hasExpiration := resource.GetAnnotations()[r.config.ExpirationLabel] + if !hasExpiration { return ctrl.Result{}, r.scheduler.DeleteTask(tag) } - schedule, scheduleErr := common.ResolveSchedule(resource.GetCreationTimestamp(), annotation) - if scheduleErr != nil { - return ctrl.Result{}, scheduleErr + date, dateErr := common.ResolveOneTimeSchedule(resource.GetCreationTimestamp(), expiration) + if dateErr != nil { + return ctrl.Result{}, dateErr } - createOrUpdateTaskErr := r.scheduler.CreateOrUpdateTask(tag, schedule, func() error { + createOrUpdateTaskErr := r.scheduler.CreateOrUpdateOneTimeTask(tag, date, func() error { logger.Info("Deleted") return client.IgnoreNotFound(r.client.Delete(ctx, resource)) diff --git a/pkg/controllers/expiration/controller_test.go b/pkg/controllers/expiration/controller_test.go index 58bd36f..54b03a5 100644 --- a/pkg/controllers/expiration/controller_test.go +++ b/pkg/controllers/expiration/controller_test.go @@ -7,14 +7,15 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" + "github.com/go-co-op/gocron/v2" + common2 "github.com/NCCloud/mayfly/mocks/github.com/NCCloud/mayfly/pkg/common" cache2 "github.com/NCCloud/mayfly/mocks/sigs.k8s.io/controller-runtime/pkg/cache" client2 "github.com/NCCloud/mayfly/mocks/sigs.k8s.io/controller-runtime/pkg/client" manager2 "github.com/NCCloud/mayfly/mocks/sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/NCCloud/mayfly/pkg/apis/v1alpha1" + "github.com/NCCloud/mayfly/pkg/apis/v1alpha2" "github.com/araddon/dateparse" - "github.com/brianvoe/gofakeit/v6" - "github.com/go-co-op/gocron/v2" "github.com/go-logr/logr" "github.com/stretchr/testify/mock" "k8s.io/apimachinery/pkg/api/errors" @@ -51,7 +52,7 @@ var testVars = struct { func init() { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(v1alpha2.AddToScheme(scheme)) kubeConfig, testEnvStartErr := (&envtest.Environment{ ControlPlane: envtest.ControlPlane{ @@ -153,7 +154,7 @@ func TestController_Reconcile(t *testing.T) { } ) - mockScheduler.EXPECT().CreateOrUpdateTask(mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockScheduler.EXPECT().CreateOrUpdateOneTimeTask(mock.Anything, mock.Anything, mock.Anything).Return(nil) mockClient.EXPECT().Get(mock.Anything, client.ObjectKeyFromObject(secret), mock.AnythingOfType("*unstructured.Unstructured")).RunAndReturn( func(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error { @@ -170,8 +171,8 @@ func TestController_Reconcile(t *testing.T) { date, _ := dateparse.ParseAny(secret. Object["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})[config. ExpirationLabel].(string)) - mockScheduler.AssertCalled(t, "CreateOrUpdateTask", "v1/Secret/my-secret/my-namespace/delete", - date, mock.Anything) + mockScheduler.AssertCalled(t, "CreateOrUpdateOneTimeTask", + "v1/Secret/my-secret/my-namespace/delete", date, mock.Anything) assert.Nil(t, reconcileErr) assert.False(t, result.Requeue) } @@ -356,7 +357,7 @@ func TestController_Reconcile_ShouldDeleteTaskWhenAnnotationValueIsPast(t *testi }, } ) - mockScheduler.EXPECT().CreateOrUpdateTask(mock.Anything, mock.Anything, mock.Anything). + mockScheduler.EXPECT().CreateOrUpdateOneTimeTask(mock.Anything, mock.Anything, mock.Anything). Return(gocron.ErrOneTimeJobStartDateTimePast) mockScheduler.EXPECT().DeleteTask(mock.Anything).Return(nil) mockClient.EXPECT().Delete(mock.Anything, mock.Anything).Return(nil) diff --git a/pkg/controllers/scheduledresource/controller.go b/pkg/controllers/scheduledresource/controller.go index 5c9d44f..8d866c8 100644 --- a/pkg/controllers/scheduledresource/controller.go +++ b/pkg/controllers/scheduledresource/controller.go @@ -4,11 +4,11 @@ import ( "context" errors2 "errors" "fmt" + "time" - "k8s.io/apimachinery/pkg/api/errors" - - "github.com/NCCloud/mayfly/pkg/apis/v1alpha1" + "github.com/NCCloud/mayfly/pkg/apis/v1alpha2" "github.com/NCCloud/mayfly/pkg/common" + "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -34,8 +34,8 @@ func NewController(config *common.Config, client client.Client, func (r *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { var ( logger = log.FromContext(ctx) - scheduledResource = &v1alpha1.ScheduledResource{} - tag = fmt.Sprintf("v1alpha1/ScheduledResource/%s/%s/create", req.Name, req.Namespace) + scheduledResource = &v1alpha2.ScheduledResource{} + tag = fmt.Sprintf("v1alpha2/ScheduledResource/%s/%s/create", req.Name, req.Namespace) ) logger.Info("Reconciliation started.") @@ -49,16 +49,19 @@ func (r *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, client.IgnoreNotFound(getErr) } - if scheduledResource.Status.Condition == v1alpha1.ConditionCreated { + oneTimeSchedule, oneTimeScheduleErr := common.ResolveOneTimeSchedule( + scheduledResource.CreationTimestamp, scheduledResource.Spec.Schedule) + isOneTimeSchedule := oneTimeScheduleErr == nil + + if isOneTimeSchedule && scheduledResource.Status.Condition == v1alpha2.ConditionFinished { return ctrl.Result{}, nil } - schedule, scheduleErr := common.ResolveSchedule(scheduledResource.CreationTimestamp, scheduledResource.Spec.In) - if scheduleErr != nil { - return ctrl.Result{}, scheduleErr - } + task := func() error { + if scheduledResource.Status.Condition == v1alpha2.ConditionFinished { + return nil + } - if createOrUpdateTaskErr := r.scheduler.CreateOrUpdateTask(tag, schedule, func() error { content, contentErr := scheduledResource.GetContent() if contentErr != nil { logger.Error(contentErr, "Error while parsing content.") @@ -77,31 +80,45 @@ func (r *Controller) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu content); client.IgnoreAlreadyExists(createErr) != nil { logger.Error(contentErr, "An error occurred while creating resource.") - scheduledResource.Status.Condition = v1alpha1.ConditionFailed + scheduledResource.Status.Condition = v1alpha2.ConditionFailed return errors2.Join(createErr, r.client.Status().Update(context.Background(), scheduledResource)) } - logger.Info(fmt.Sprintf("%s created.", tag)) + logger.Info(fmt.Sprintf("Task %s finished.", tag)) - _ = r.scheduler.DeleteTask(tag) + if isOneTimeSchedule { + _ = r.scheduler.DeleteTask(tag) + scheduledResource.Status.Condition = v1alpha2.ConditionFinished + } - scheduledResource.Status.Condition = v1alpha1.ConditionCreated + scheduledResource.Status.NextRun = r.scheduler.GetTaskNextRun(tag) + scheduledResource.Status.LastRun = time.Now().Format(time.RFC3339) return r.client.Status().Update(context.Background(), scheduledResource) - }); createOrUpdateTaskErr != nil { - logger.Error(createOrUpdateTaskErr, "Error while creating or updating task.") + } - return ctrl.Result{}, createOrUpdateTaskErr + var createOrUpdateErr error + if isOneTimeSchedule { + createOrUpdateErr = r.scheduler.CreateOrUpdateOneTimeTask(tag, oneTimeSchedule, task) + } else { + createOrUpdateErr = r.scheduler.CreateOrUpdateRecurringTask(tag, scheduledResource.Spec.Schedule, task) } - scheduledResource.Status.Condition = v1alpha1.ConditionScheduled + scheduledResource.Status.NextRun = r.scheduler.GetTaskNextRun(tag) + + if createOrUpdateErr != nil { + scheduledResource.Status.Condition = v1alpha2.ConditionFailed + } else { + scheduledResource.Status.Condition = v1alpha2.ConditionScheduled + } - return ctrl.Result{}, r.client.Status().Update(ctx, scheduledResource) + return ctrl.Result{}, errors2.Join(createOrUpdateErr, + r.client.Status().Update(context.Background(), scheduledResource)) } func (r *Controller) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.ScheduledResource{}). + For(&v1alpha2.ScheduledResource{}). Complete(r) } diff --git a/pkg/controllers/scheduledresource/controller_test.go b/pkg/controllers/scheduledresource/controller_test.go index a06db15..a2c95d2 100644 --- a/pkg/controllers/scheduledresource/controller_test.go +++ b/pkg/controllers/scheduledresource/controller_test.go @@ -9,11 +9,12 @@ import ( "testing" "time" + "github.com/NCCloud/mayfly/pkg/apis/v1alpha2" + common2 "github.com/NCCloud/mayfly/mocks/github.com/NCCloud/mayfly/pkg/common" cache2 "github.com/NCCloud/mayfly/mocks/sigs.k8s.io/controller-runtime/pkg/cache" client2 "github.com/NCCloud/mayfly/mocks/sigs.k8s.io/controller-runtime/pkg/client" manager2 "github.com/NCCloud/mayfly/mocks/sigs.k8s.io/controller-runtime/pkg/manager" - "github.com/NCCloud/mayfly/pkg/apis/v1alpha1" "github.com/NCCloud/mayfly/pkg/common" "github.com/araddon/dateparse" "github.com/brianvoe/gofakeit/v6" @@ -51,7 +52,7 @@ var testVars = struct { func init() { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(v1alpha2.AddToScheme(scheme)) kubeConfig, testEnvStartErr := (&envtest.Environment{ ControlPlane: envtest.ControlPlane{ @@ -131,24 +132,26 @@ func TestController_Reconcile(t *testing.T) { mockStatusClient = new(client2.MockSubResourceClient) mockScheduler = new(common2.MockScheduler) controller = NewController(common.NewConfig(), mockClient, mockScheduler) - scheduledResource = &v1alpha1.ScheduledResource{ + scheduledResource = &v1alpha2.ScheduledResource{ ObjectMeta: metav1.ObjectMeta{ Name: gofakeit.Name(), Namespace: gofakeit.Name(), }, - Spec: v1alpha1.ScheduledResourceSpec{ - In: gofakeit.FutureDate().String(), + Spec: v1alpha2.ScheduledResourceSpec{ + Schedule: gofakeit.FutureDate().String(), }, } ) - mockScheduler.EXPECT().CreateOrUpdateTask(mock.Anything, mock.Anything, mock.Anything).Return(nil) + now := time.Now().String() + mockScheduler.EXPECT().CreateOrUpdateOneTimeTask(mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockScheduler.EXPECT().GetTaskNextRun(mock.Anything).Return(now) mockStatusClient.EXPECT().Update(mock.Anything, mock.Anything).Return(nil) mockClient.EXPECT().Status().Return(mockStatusClient) mockClient.EXPECT().Get(mock.Anything, client.ObjectKeyFromObject(scheduledResource), - mock.AnythingOfType("*v1alpha1.ScheduledResource")).RunAndReturn( + mock.AnythingOfType("*v1alpha2.ScheduledResource")).RunAndReturn( func(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error { - scheduledResource.DeepCopyInto(obj.(*v1alpha1.ScheduledResource)) + scheduledResource.DeepCopyInto(obj.(*v1alpha2.ScheduledResource)) return nil }) @@ -158,27 +161,28 @@ func TestController_Reconcile(t *testing.T) { }) // then - date, _ := dateparse.ParseAny(scheduledResource.Spec.In) - mockScheduler.AssertCalled(t, "CreateOrUpdateTask", fmt.Sprintf("v1alpha1/ScheduledResource/%s/%s/create", - scheduledResource.Name, scheduledResource.Namespace), date, mock.Anything) + date, _ := dateparse.ParseAny(scheduledResource.Spec.Schedule) + mockScheduler.AssertCalled(t, "CreateOrUpdateOneTimeTask", + fmt.Sprintf("v1alpha2/ScheduledResource/%s/%s/create", + scheduledResource.Name, scheduledResource.Namespace), date, mock.Anything) mockStatusClient.AssertCalled(t, "Update", mock.Anything, mock.MatchedBy(func(obj client.Object) bool { - return obj.(*v1alpha1.ScheduledResource).Status.Condition == v1alpha1.ConditionScheduled + return obj.(*v1alpha2.ScheduledResource).Status.Condition == v1alpha2.ConditionScheduled })) assert.Nil(t, reconcileErr) assert.False(t, result.Requeue) } -func TestController_ReconcileIntegration(t *testing.T) { +func TestController_ReconcileIntegration_DurationSchedule(t *testing.T) { // given var ( ctx = context.Background() - scheduledResource = &v1alpha1.ScheduledResource{ + scheduledResource = &v1alpha2.ScheduledResource{ ObjectMeta: metav1.ObjectMeta{ Name: strings.ToLower(strings.ReplaceAll(gofakeit.Name(), " ", "")), Namespace: "default", }, - Spec: v1alpha1.ScheduledResourceSpec{ - In: "5s", + Spec: v1alpha2.ScheduledResourceSpec{ + Schedule: "5s", Content: `apiVersion: v1 kind: Secret metadata: @@ -196,7 +200,74 @@ metadata: assert.Nil(t, contentErr) assert.Nil(t, createErr) assert.Eventually(t, func() bool { - return errors.IsNotFound(testVars.k8sClient.Get(ctx, client.ObjectKeyFromObject(content), content)) + return testVars.k8sClient.Get(ctx, client.ObjectKeyFromObject(content), content) == nil + }, 60*time.Second, 100*time.Millisecond) +} + +func TestController_ReconcileIntegration_ExactDateSchedule(t *testing.T) { + // given + var ( + ctx = context.Background() + scheduledResource = &v1alpha2.ScheduledResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: strings.ToLower(strings.ReplaceAll(gofakeit.Name(), " ", "")), + Namespace: "default", + }, + Spec: v1alpha2.ScheduledResourceSpec{ + Content: `apiVersion: v1 +kind: Secret +metadata: + name: my-resource + namespace: default`, + }, + } + ) + + // when + scheduledResource.Spec.Schedule = time.Now().Add(5 * time.Second).String() + content, contentErr := scheduledResource.GetContent() + createErr := testVars.k8sClient.Create(ctx, scheduledResource) + + // then + assert.Nil(t, contentErr) + assert.Nil(t, createErr) + assert.Eventually(t, func() bool { + return testVars.k8sClient.Get(ctx, client.ObjectKeyFromObject(content), content) == nil + }, 60*time.Second, 100*time.Millisecond) +} + +func TestController_ReconcileIntegration_CronSchedule(t *testing.T) { + // given + var ( + ctx = context.Background() + scheduledResource = &v1alpha2.ScheduledResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: strings.ToLower(strings.ReplaceAll(gofakeit.Name(), " ", "")), + Namespace: "default", + }, + Spec: v1alpha2.ScheduledResourceSpec{ + Schedule: "*/5 * * * * *", + Content: `apiVersion: v1 +kind: Secret +metadata: + name: my-resource + namespace: default`, + }, + } + ) + + // when + content, contentErr := scheduledResource.GetContent() + createErr := testVars.k8sClient.Create(ctx, scheduledResource) + + // then + assert.Nil(t, contentErr) + assert.Nil(t, createErr) + assert.Eventually(t, func() bool { + return testVars.k8sClient.Get(ctx, client.ObjectKeyFromObject(content), content) == nil + }, 60*time.Second, 100*time.Millisecond) + assert.Eventually(t, func() bool { + return testVars.k8sClient.Delete(ctx, content) == nil }, 60*time.Second, 100*time.Millisecond) assert.Eventually(t, func() bool { return testVars.k8sClient.Get(ctx, client.ObjectKeyFromObject(content), content) == nil @@ -214,7 +285,7 @@ func TestController_Reconcile_ShouldDeleteTaskWhenNotFound(t *testing.T) { mockScheduler.EXPECT().DeleteTask(mock.Anything).Return(nil) mockClient.EXPECT().Get(mock.Anything, mock.Anything, - mock.AnythingOfType("*v1alpha1.ScheduledResource")).Return(errors.NewNotFound(schema.GroupResource{}, "")) + mock.AnythingOfType("*v1alpha2.ScheduledResource")).Return(errors.NewNotFound(schema.GroupResource{}, "")) // when result, reconcileErr := controller.Reconcile(ctx, ctrl.Request{ @@ -225,7 +296,7 @@ func TestController_Reconcile_ShouldDeleteTaskWhenNotFound(t *testing.T) { }) // then - mockScheduler.AssertCalled(t, "DeleteTask", fmt.Sprintf("v1alpha1/ScheduledResource/%s/%s/create", + mockScheduler.AssertCalled(t, "DeleteTask", fmt.Sprintf("v1alpha2/ScheduledResource/%s/%s/create", "my-secret", "my-namespace")) assert.Nil(t, reconcileErr) assert.False(t, result.Requeue) @@ -236,23 +307,29 @@ func TestController_Reconcile_ShouldReturnErrWhenInFieldDoesNotMakeSense(t *test var ( ctx = context.Background() mockClient = new(client2.MockClient) + mockStatusClient = new(client2.MockSubResourceClient) mockScheduler = new(common2.MockScheduler) controller = NewController(common.NewConfig(), mockClient, mockScheduler) - scheduledResource = &v1alpha1.ScheduledResource{ + scheduledResource = &v1alpha2.ScheduledResource{ ObjectMeta: metav1.ObjectMeta{ Name: gofakeit.Name(), Namespace: gofakeit.Name(), }, - Spec: v1alpha1.ScheduledResourceSpec{ - In: gofakeit.BeerName(), + Spec: v1alpha2.ScheduledResourceSpec{ + Schedule: gofakeit.BeerName(), }, } ) + mockClient.EXPECT().Status().Return(mockStatusClient) + mockStatusClient.EXPECT().Update(mock.Anything, mock.Anything).Return(nil) + mockScheduler.EXPECT().CreateOrUpdateRecurringTask(mock.Anything, mock.Anything, mock.Anything). + Return(errors2.New("unparsable schedule")) + mockScheduler.EXPECT().GetTaskNextRun(mock.Anything).Return("") mockClient.EXPECT().Get(mock.Anything, client.ObjectKeyFromObject(scheduledResource), - mock.AnythingOfType("*v1alpha1.ScheduledResource")).RunAndReturn( + mock.AnythingOfType("*v1alpha2.ScheduledResource")).RunAndReturn( func(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error { - scheduledResource.DeepCopyInto(obj.(*v1alpha1.ScheduledResource)) + scheduledResource.DeepCopyInto(obj.(*v1alpha2.ScheduledResource)) return nil }) @@ -264,9 +341,12 @@ func TestController_Reconcile_ShouldReturnErrWhenInFieldDoesNotMakeSense(t *test // then assert.NotNil(t, reconcileErr) assert.False(t, result.Requeue) + mockStatusClient.AssertCalled(t, "Update", mock.Anything, mock.MatchedBy(func(obj client.Object) bool { + return obj.(*v1alpha2.ScheduledResource).Status.Condition == v1alpha2.ConditionFailed + })) } -func TestController_Reconcile_TaskShouldCreateItem(t *testing.T) { +func TestController_Reconcile_TaskShouldCreateObject(t *testing.T) { // given var ( ctx = context.Background() @@ -274,13 +354,13 @@ func TestController_Reconcile_TaskShouldCreateItem(t *testing.T) { mockStatusClient = new(client2.MockSubResourceClient) mockScheduler = new(common2.MockScheduler) controller = NewController(common.NewConfig(), mockClient, mockScheduler) - scheduledResource = &v1alpha1.ScheduledResource{ + scheduledResource = &v1alpha2.ScheduledResource{ ObjectMeta: metav1.ObjectMeta{ Name: gofakeit.Name(), Namespace: gofakeit.Name(), }, - Spec: v1alpha1.ScheduledResourceSpec{ - In: gofakeit.FutureDate().String(), + Spec: v1alpha2.ScheduledResourceSpec{ + Schedule: gofakeit.FutureDate().String(), Content: `apiVersion: v1 kind: Secret metadata: @@ -291,13 +371,14 @@ metadata: ) mockScheduler.EXPECT().DeleteTask(mock.Anything).Return(nil) - mockScheduler.EXPECT().CreateOrUpdateTask(mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockScheduler.EXPECT().CreateOrUpdateOneTimeTask(mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockScheduler.EXPECT().GetTaskNextRun(mock.Anything).Return("") mockStatusClient.EXPECT().Update(mock.Anything, mock.Anything).Return(nil) mockClient.EXPECT().Status().Return(mockStatusClient) mockClient.EXPECT().Get(mock.Anything, client.ObjectKeyFromObject(scheduledResource), - mock.AnythingOfType("*v1alpha1.ScheduledResource")).RunAndReturn( + mock.AnythingOfType("*v1alpha2.ScheduledResource")).RunAndReturn( func(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error { - scheduledResource.DeepCopyInto(obj.(*v1alpha1.ScheduledResource)) + scheduledResource.DeepCopyInto(obj.(*v1alpha2.ScheduledResource)) return nil }) mockClient.EXPECT().Create(mock.Anything, mock.Anything).Return(nil) @@ -309,9 +390,12 @@ metadata: taskErr := mockScheduler.Calls[0].Arguments[2].(func() error)() // then - mockScheduler.AssertCalled(t, "DeleteTask", fmt.Sprintf("v1alpha1/ScheduledResource/%s/%s/create", + mockScheduler.AssertCalled(t, "DeleteTask", fmt.Sprintf("v1alpha2/ScheduledResource/%s/%s/create", scheduledResource.Name, scheduledResource.Namespace)) mockClient.AssertCalled(t, "Create", mock.Anything, mock.Anything) + mockStatusClient.AssertCalled(t, "Update", mock.Anything, mock.MatchedBy(func(obj client.Object) bool { + return obj.(*v1alpha2.ScheduledResource).Status.Condition == v1alpha2.ConditionFinished + })) assert.Nil(t, reconcileErr) assert.Nil(t, taskErr) } @@ -324,13 +408,13 @@ func TestController_Reconcile_ShouldFailedIfAnyErrorHappens(t *testing.T) { mockStatusClient = new(client2.MockSubResourceClient) mockScheduler = new(common2.MockScheduler) controller = NewController(common.NewConfig(), mockClient, mockScheduler) - scheduledResource = &v1alpha1.ScheduledResource{ + scheduledResource = &v1alpha2.ScheduledResource{ ObjectMeta: metav1.ObjectMeta{ Name: gofakeit.Name(), Namespace: gofakeit.Name(), }, - Spec: v1alpha1.ScheduledResourceSpec{ - In: gofakeit.FutureDate().String(), + Spec: v1alpha2.ScheduledResourceSpec{ + Schedule: gofakeit.FutureDate().String(), Content: `apiVersion: v1 kind: Secret metadata: @@ -341,13 +425,14 @@ metadata: ) mockScheduler.EXPECT().DeleteTask(mock.Anything).Return(nil) - mockScheduler.EXPECT().CreateOrUpdateTask(mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockScheduler.EXPECT().CreateOrUpdateOneTimeTask(mock.Anything, mock.Anything, mock.Anything).Return(nil) + mockScheduler.EXPECT().GetTaskNextRun(mock.Anything).Return("") mockStatusClient.EXPECT().Update(mock.Anything, mock.Anything).Return(nil) mockClient.EXPECT().Status().Return(mockStatusClient) mockClient.EXPECT().Get(mock.Anything, client.ObjectKeyFromObject(scheduledResource), - mock.AnythingOfType("*v1alpha1.ScheduledResource")).RunAndReturn( + mock.AnythingOfType("*v1alpha2.ScheduledResource")).RunAndReturn( func(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error { - scheduledResource.DeepCopyInto(obj.(*v1alpha1.ScheduledResource)) + scheduledResource.DeepCopyInto(obj.(*v1alpha2.ScheduledResource)) return nil }) mockClient.EXPECT().Create(mock.Anything, mock.Anything).Return(errors2.New("an error")) @@ -360,7 +445,7 @@ metadata: // then mockStatusClient.AssertCalled(t, "Update", mock.Anything, mock.MatchedBy(func(obj client.Object) bool { - return obj.(*v1alpha1.ScheduledResource).Status.Condition == v1alpha1.ConditionFailed + return obj.(*v1alpha2.ScheduledResource).Status.Condition == v1alpha2.ConditionFailed })) assert.Nil(t, reconcileErr) assert.NotNil(t, taskErr) @@ -377,7 +462,7 @@ func TestController_SetupWithManager(t *testing.T) { scheme = runtime.NewScheme() ) - addToSchemeErr := v1alpha1.AddToScheme(scheme) + addToSchemeErr := v1alpha2.AddToScheme(scheme) mockManager.EXPECT().GetControllerOptions().Return(config.Controller{}) mockManager.EXPECT().GetScheme().Return(scheme) mockManager.EXPECT().GetCache().Return(mockCache)