Repository: vshn/k8up
Pull request #260 (2021-01-05 17:20:32)

Pass Scheduler Backend Config to Childresources
## Summary

This PR fixes a regression by which k8up did not apply the _backend_ section of a _Schedule_ custom resource to the created sub-resources, i.e. _Backup_, _Check_, _Prune_, etc.

This fixes one of the findings in #256.

## Checklist
<!--
Remove items that do not apply. For completed items, change [ ] to [x].
-->

- [x] Keep pull requests small so they can be easily reviewed.
- [x] Categorize the PR by setting a good title and adding one of the labels:
      `bug`, `enhancement`, `documentation`, `change`, `breaking`,
      as they show up in the changelog
- [x] Update the documentation.
- [x] Update tests.
- [x] Link this PR to related issues.

<!--
NOTE: these things are not required to open a PR and can be done afterwards,
while the PR is open.
-->


ccremer: Since you touched resource merging stuff I tested resource templates in this branch, to see how that works now.

We basically have 3 levels where we can specify resources, each overrides the previous one:
1. Global resource limits and requests
2. `resourceRequirementsTemplate` in the schedule Spec
3. `resources` of a job spec (e.g. prune)

This PR has the behaviour so that the `Schedule` after reconciling has additional resources specified in the Job spec.
e.g. 
```yaml
---
apiVersion: backup.appuio.ch/v1alpha1
kind: Schedule
metadata:
  name: schedule-test
spec:
  resourceRequirementsTemplate:
    requests:
      cpu: "250m"
  prune:
    schedule: '*/1 * * * *'
```
becomes
```yaml
apiVersion: backup.appuio.ch/v1alpha1
kind: Schedule
metadata:
  name: schedule-test
  namespace: default
spec:
...
  prune:
    resources:
      requests:
        cpu: 250m
    retention:
      keepDaily: 14
      keepLast: 5
    schedule: '*/1 * * * *'
  resourceRequirementsTemplate:
    requests:
      cpu: 250m
```
(only important thing: `spec.prune.resources` got added)
And the prune spec gets as expected:
```yaml
apiVersion: backup.appuio.ch/v1alpha1
kind: Prune
metadata:
  name: prunejob-1609946760
  namespace: default
spec:
  resources:
    requests:
      cpu: 250m
...
```
All the while the operator has this env var set: `kubectl set env deployment/k8up-operator BACKUP_GLOBALMEMORY_LIMIT=76M -n k8up-system`. But the `prunejob-1609946760` CR doesn't get the memory limit.

There are 2 issues:
1. The `resources` of a specific Job spec in `test-schedule` gets added after reconciling. I think this shouldn't be as it would make changing a value in `resourceRequirementsTemplate` impossible because `spec.<job>.resources` is more specific. Only when creating a `Prune` object, the final values should be in there, but not in the `Schedule`
2. The global resource values aren't applied at all. Though this could already be the case in the current master branch. Insufficient testing? :thinking: In any case, if `spec.<job>.resources` again gets set in the spec, it would disallow cluster administrators to effectively alter resource requirements after the first reconciliation (given users don't override this in the initial source YAML themselves, then it's their responsibility to meet cluster resource policies).

In code this means that merged resources should be kept as runtime variables and stored in the `Prune` CR. And not stored in the Schedule spec after reconciliation.
See also #143

Would you like a separate issue for this bug? Or do you think you can handle this in this PR when you're already touching the code anyway?
cimnine: > The global resource values aren't applied at all. Though this could already be the case in the current master branch.

I have not confirmed this, but since I did not actually touch that logic I suspect so. I believe that this can be handled in a separate issue.

> In code this means that merged resources should be kept as runtime variables and stored in the Prune CR. And not stored in the Schedule spec after reconciliation.

I have changed to logic accordingly. The reconciliation loop still creates the `Resources` key for some reason I have not been able to figure out yet (but I have stopped looking for now as it's just a _Schoenheitsfehler_, IMO).
ccremer: Running the current branch and applying the default sample actually throws panics:
```console
2021-01-08T10:21:15.355Z	INFO	controllers.Schedule	registering schedule for	{"schedule": "default/schedule-test", "type": "prune", "schedule": "*/2 * * * *"}
2021-01-08T10:21:15.355Z	DEBUG	controller	Successfully Reconciled	{"reconcilerGroup": "backup.appuio.ch", "reconcilerKind": "Schedule", "controller": "schedule", "name": "schedule-test", "namespace": "default"}
E0108 10:21:15.356032       1 runtime.go:78] Observed a panic: &reflect.ValueError{Method:"reflect.Value.Type", Kind:0x0} (reflect: call of reflect.Value.Type on zero Value)
goroutine 312 [running]:
k8s.io/apimachinery/pkg/util/runtime.logPanic(0x1514a80, 0xc00067fe20)
	/home/chris/go/pkg/mod/k8s.io/apimachinery@v0.18.10/pkg/util/runtime/runtime.go:74 +0xa6
k8s.io/apimachinery/pkg/util/runtime.HandleCrash(0x0, 0x0, 0x0)
	/home/chris/go/pkg/mod/k8s.io/apimachinery@v0.18.10/pkg/util/runtime/runtime.go:48 +0x89
panic(0x1514a80, 0xc00067fe20)
	/snap/go/6439/src/runtime/panic.go:969 +0x175
reflect.Value.Type(0x0, 0x0, 0x0, 0x18d19a0, 0x1625f20)
	/snap/go/6439/src/reflect/value.go:1894 +0x189
github.com/imdario/mergo.merge(0x15804a0, 0xc00067d6c0, 0x15804a0, 0x0, 0x0, 0x0, 0x0, 0x16e3327, 0xc)
	/home/chris/go/pkg/mod/github.com/imdario/mergo@v0.3.11/merge.go:358 +0x20a
github.com/imdario/mergo.Merge(...)
	/home/chris/go/pkg/mod/github.com/imdario/mergo@v0.3.11/merge.go:291
github.com/vshn/k8up/handler.(*ScheduleHandler).mergeBackendWithDefaults(0xc0006c7c48, 0xc00067d6c0)
	/home/chris/Documents/source/vshn/k8up/handler/schedule.go:152 +0x79
github.com/vshn/k8up/handler.(*ScheduleHandler).mergeWithDefaults(0xc0006c7c48, 0xc0004e3620)
	/home/chris/Documents/source/vshn/k8up/handler/schedule.go:139 +0x65
github.com/vshn/k8up/handler.(*ScheduleHandler).createJobList(0xc0006c7c48, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
	/home/chris/Documents/source/vshn/k8up/handler/schedule.go:91 +0x7bc
github.com/vshn/k8up/handler.(*ScheduleHandler).Handle(0xc0006c7c48, 0x8, 0xc0006c7b78)
	/home/chris/Documents/source/vshn/k8up/handler/schedule.go:54 +0x85
```
cimnine: > Running the current branch and applying the default sample actually throws panics:

Interesting. My operator was running the whole night and I have not observed such a panic. Is it reproducible on your end? And would you mind sharing the actual schedule definition, since there are many floating around in the docs and the repo.
ccremer: > Interesting. My operator was running the whole night and I have not observed such a panic. Is it reproducible on your end? And would you mind sharing the actual schedule definition, since there are many floating around in the docs and the repo.

```bash
make clean e2e-test
k apply -f config/samples/k8up_v1alpha1_schedule.yaml
```
cimnine: When I run `k apply -f config/samples/k8up_v1alpha1_schedule.yaml` against the correct cluster it indeed crashed my k8up as well. This has now been fixed.

Patch:
diff --git a/api/v1alpha1/backup_types.go b/api/v1alpha1/backup_types.go
index 99ab37f4..346a0c55 100644
--- a/api/v1alpha1/backup_types.go
+++ b/api/v1alpha1/backup_types.go
@@ -10,23 +10,22 @@ import (
 // the backup repository when applied. If used with defaults or schedules the operator will
 // ensure that the defaults are applied before creating the object on the API.
 type BackupSpec struct {
-	// Backend contains the restic repo where the job should backup to.
-	Backend *Backend `json:"backend,omitempty"`
+	RunnableSpec `json:",inline"`
+
 	// KeepJobs amount of jobs to keep for later analysis
 	KeepJobs *int `json:"keepJobs,omitempty"`
 
 	// PromURL sets a prometheus push URL where the backup container send metrics to
 	// +optional
 	PromURL string `json:"promURL,omitempty"`
+
 	// StatsURL sets an arbitrary URL where the wrestic container posts metrics and
 	// information about the snapshots to. This is in addition to the prometheus
 	// pushgateway.
 	StatsURL string `json:"statsURL,omitempty"`
+
 	// Tags is a list of arbitrary tags that get added to the backup via Restic's tagging system
 	Tags []string `json:"tags,omitempty"`
-
-	// Resources describes the compute resource requirements (cpu, memory, etc.)
-	Resources corev1.ResourceRequirements `json:"resources,omitempty"`
 }
 
 type BackupTemplate struct {
diff --git a/api/v1alpha1/check_types.go b/api/v1alpha1/check_types.go
index 34f1caff..288c08a7 100644
--- a/api/v1alpha1/check_types.go
+++ b/api/v1alpha1/check_types.go
@@ -9,11 +9,14 @@ import (
 // CheckSpec defines the desired state of Check. It needs to contain the repository
 // information.
 type CheckSpec struct {
-	PromURL  string   `json:"promURL,omitempty"`
-	Backend  *Backend `json:"backend,omitempty"`
-	KeepJobs *int     `json:"keepJobs,omitempty"`
-	// Resources describes the compute resource requirements (cpu, memory, etc.)
-	Resources corev1.ResourceRequirements `json:"resources,omitempty"`
+	RunnableSpec `json:",inline"`
+
+	// PromURL sets a prometheus push URL where the backup container send metrics to
+	// +optional
+	PromURL string `json:"promURL,omitempty"`
+
+	// KeepJobs amount of jobs to keep for later analysis
+	KeepJobs *int `json:"keepJobs,omitempty"`
 }
 
 // +kubebuilder:object:root=true
diff --git a/api/v1alpha1/status.go b/api/v1alpha1/common_types.go
similarity index 56%
rename from api/v1alpha1/status.go
rename to api/v1alpha1/common_types.go
index a6f748db..b7a72df4 100644
--- a/api/v1alpha1/status.go
+++ b/api/v1alpha1/common_types.go
@@ -5,6 +5,25 @@ import (
 	"github.com/operator-framework/operator-lib/status" // to be replaces with `metav1` from above
 )
 
+// JobType defines what job type this is.
+type JobType string
+
+// The jobs types that k8up deals with
+const (
+	BackupType   JobType = "backup"
+	CheckType    JobType = "check"
+	ArchiveType  JobType = "archive"
+	RestoreType  JobType = "restore"
+	PruneType    JobType = "prune"
+	ScheduleType JobType = "schedule"
+)
+
+// String casts the value to string.
+// "aJobType.String()" and "string(aJobType)" are equivalent.
+func (j JobType) String() string {
+	return string(j)
+}
+
 // Status defines the observed state of a generic K8up job. It is used for the
 // operator to determine what to do.
 type Status struct {
diff --git a/api/v1alpha1/job_types.go b/api/v1alpha1/job_types.go
deleted file mode 100644
index 3e474270..00000000
--- a/api/v1alpha1/job_types.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package v1alpha1
-
-type (
-	// JobType defines what job type this is.
-	JobType string
-)
-
-const (
-	BackupType   JobType = "backup"
-	CheckType    JobType = "check"
-	ArchiveType  JobType = "archive"
-	RestoreType  JobType = "restore"
-	PruneType    JobType = "prune"
-	ScheduleType JobType = "schedule"
-)
diff --git a/api/v1alpha1/prune_types.go b/api/v1alpha1/prune_types.go
index 9a6b26cc..e01e07f8 100644
--- a/api/v1alpha1/prune_types.go
+++ b/api/v1alpha1/prune_types.go
@@ -9,12 +9,11 @@ import (
 // PruneSpec needs to contain the repository information as well as the desired
 // retention policies.
 type PruneSpec struct {
+	RunnableSpec `json:",inline"`
+
 	// Retention sets how many backups should be kept after a forget and prune
 	Retention RetentionPolicy `json:"retention,omitempty"`
-	Backend   *Backend        `json:"backend,omitempty"`
 	KeepJobs  *int            `json:"keepJobs,omitempty"`
-	// Resources describes the compute resource requirements (cpu, memory, etc.)
-	Resources corev1.ResourceRequirements `json:"resources,omitempty"`
 }
 
 func (p PruneSpec) CreateObject(name, namespace string) runtime.Object {
diff --git a/api/v1alpha1/restore_types.go b/api/v1alpha1/restore_types.go
index 879ee8e1..e5c87cee 100644
--- a/api/v1alpha1/restore_types.go
+++ b/api/v1alpha1/restore_types.go
@@ -9,16 +9,14 @@ import (
 // RestoreSpec can either contain an S3 restore point or a local one. For the local
 // one you need to define an existing PVC.
 type RestoreSpec struct {
-	// Backend contains the backend information
-	Backend       *Backend       `json:"backend,omitempty"`
+	RunnableSpec `json:",inline"`
+
 	RestoreMethod *RestoreMethod `json:"restoreMethod,omitempty"`
 	RestoreFilter string         `json:"restoreFilter,omitempty"`
 	Snapshot      string         `json:"snapshot,omitempty"`
 	KeepJobs      *int           `json:"keepJobs,omitempty"`
 	// Tags is a list of arbitrary tags that get added to the backup via Restic's tagging system
 	Tags []string `json:"tags,omitempty"`
-	// Resources describes the compute resource requirements (cpu, memory, etc.)
-	Resources corev1.ResourceRequirements `json:"resources,omitempty"`
 }
 
 func (r RestoreSpec) CreateObject(name, namespace string) runtime.Object {
diff --git a/api/v1alpha1/runnable_types.go b/api/v1alpha1/runnable_types.go
new file mode 100644
index 00000000..b3482d9b
--- /dev/null
+++ b/api/v1alpha1/runnable_types.go
@@ -0,0 +1,14 @@
+package v1alpha1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+)
+
+// RunnableSpec defines the fields that are necessary on the specs of all actions that are translated to k8s jobs eventually.
+type RunnableSpec struct {
+	// Backend contains the restic repo where the job should backup to.
+	Backend *Backend `json:"backend,omitempty"`
+
+	// Resources describes the compute resource requirements (cpu, memory, etc.)
+	Resources corev1.ResourceRequirements `json:"resources,omitempty"`
+}
diff --git a/api/v1alpha1/schedule_types.go b/api/v1alpha1/schedule_types.go
index f778e925..efa72028 100644
--- a/api/v1alpha1/schedule_types.go
+++ b/api/v1alpha1/schedule_types.go
@@ -1,6 +1,8 @@
 package v1alpha1
 
 import (
+	"strings"
+
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -19,10 +21,13 @@ type ScheduleSpec struct {
 	ResourceRequirementsTemplate corev1.ResourceRequirements `json:"resourceRequirementsTemplate,omitempty"`
 }
 
+// ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
+type ScheduleDefinition string
+
 // ScheduleCommon contains fields every schedule needs
 type ScheduleCommon struct {
-	Schedule              string `json:"schedule,omitempty"`
-	ConcurrentRunsAllowed bool   `json:"concurrentRunsAllowed,omitempty"`
+	Schedule              ScheduleDefinition `json:"schedule,omitempty"`
+	ConcurrentRunsAllowed bool               `json:"concurrentRunsAllowed,omitempty"`
 }
 
 // RestoreSchedule manages schedules for the restore service
@@ -57,7 +62,7 @@ type PruneSchedule struct {
 // ScheduleStatus defines the observed state of Schedule
 type ScheduleStatus struct {
 	// EffectiveSchedules displays the final schedule for each type (useful when using smart schedules).
-	EffectiveSchedules map[JobType]string `json:"effectiveSchedules,omitempty"`
+	EffectiveSchedules map[JobType]ScheduleDefinition `json:"effectiveSchedules,omitempty"`
 }
 
 // +kubebuilder:object:root=true
@@ -104,3 +109,23 @@ func (s *Schedule) GetStatus() *Status {
 func (s *Schedule) GetResources() corev1.ResourceRequirements {
 	return s.Spec.ResourceRequirementsTemplate
 }
+
+// String casts the value to string.
+// "aScheduleDefinition.String()" and "string(aScheduleDefinition)" are equivalent.
+func (s ScheduleDefinition) String() string {
+	return string(s)
+}
+
+// IsNonStandard returns true if the value begins with "@",
+// indicating a special definition.
+// Two examples are '@daily' and '@daily-random'.
+func (s ScheduleDefinition) IsNonStandard() bool {
+	return strings.HasPrefix(string(s), "@")
+}
+
+// IsRandom is true if the value is a special definition (as indicated by IsNonStandard)
+// and if it ends with '-random'.
+// Two examples are '@daily-random' and '@weekly-random'.
+func (s ScheduleDefinition) IsRandom() bool {
+	return s.IsNonStandard() && strings.HasSuffix(string(s), "-random")
+}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 42df4bda..2f42cb71 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -301,11 +301,7 @@ func (in *BackupSchedule) DeepCopy() *BackupSchedule {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *BackupSpec) DeepCopyInto(out *BackupSpec) {
 	*out = *in
-	if in.Backend != nil {
-		in, out := &in.Backend, &out.Backend
-		*out = new(Backend)
-		(*in).DeepCopyInto(*out)
-	}
+	in.RunnableSpec.DeepCopyInto(&out.RunnableSpec)
 	if in.KeepJobs != nil {
 		in, out := &in.KeepJobs, &out.KeepJobs
 		*out = new(int)
@@ -316,7 +312,6 @@ func (in *BackupSpec) DeepCopyInto(out *BackupSpec) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
-	in.Resources.DeepCopyInto(&out.Resources)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSpec.
@@ -438,17 +433,12 @@ func (in *CheckSchedule) DeepCopy() *CheckSchedule {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CheckSpec) DeepCopyInto(out *CheckSpec) {
 	*out = *in
-	if in.Backend != nil {
-		in, out := &in.Backend, &out.Backend
-		*out = new(Backend)
-		(*in).DeepCopyInto(*out)
-	}
+	in.RunnableSpec.DeepCopyInto(&out.RunnableSpec)
 	if in.KeepJobs != nil {
 		in, out := &in.KeepJobs, &out.KeepJobs
 		*out = new(int)
 		**out = **in
 	}
-	in.Resources.DeepCopyInto(&out.Resources)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheckSpec.
@@ -713,18 +703,13 @@ func (in *PruneSchedule) DeepCopy() *PruneSchedule {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PruneSpec) DeepCopyInto(out *PruneSpec) {
 	*out = *in
+	in.RunnableSpec.DeepCopyInto(&out.RunnableSpec)
 	in.Retention.DeepCopyInto(&out.Retention)
-	if in.Backend != nil {
-		in, out := &in.Backend, &out.Backend
-		*out = new(Backend)
-		(*in).DeepCopyInto(*out)
-	}
 	if in.KeepJobs != nil {
 		in, out := &in.KeepJobs, &out.KeepJobs
 		*out = new(int)
 		**out = **in
 	}
-	in.Resources.DeepCopyInto(&out.Resources)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PruneSpec.
@@ -870,11 +855,7 @@ func (in *RestoreSchedule) DeepCopy() *RestoreSchedule {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
 	*out = *in
-	if in.Backend != nil {
-		in, out := &in.Backend, &out.Backend
-		*out = new(Backend)
-		(*in).DeepCopyInto(*out)
-	}
+	in.RunnableSpec.DeepCopyInto(&out.RunnableSpec)
 	if in.RestoreMethod != nil {
 		in, out := &in.RestoreMethod, &out.RestoreMethod
 		*out = new(RestoreMethod)
@@ -890,7 +871,6 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
-	in.Resources.DeepCopyInto(&out.Resources)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreSpec.
@@ -933,6 +913,27 @@ func (in *RetentionPolicy) DeepCopy() *RetentionPolicy {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RunnableSpec) DeepCopyInto(out *RunnableSpec) {
+	*out = *in
+	if in.Backend != nil {
+		in, out := &in.Backend, &out.Backend
+		*out = new(Backend)
+		(*in).DeepCopyInto(*out)
+	}
+	in.Resources.DeepCopyInto(&out.Resources)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunnableSpec.
+func (in *RunnableSpec) DeepCopy() *RunnableSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(RunnableSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *S3Spec) DeepCopyInto(out *S3Spec) {
 	*out = *in
@@ -1088,7 +1089,7 @@ func (in *ScheduleStatus) DeepCopyInto(out *ScheduleStatus) {
 	*out = *in
 	if in.EffectiveSchedules != nil {
 		in, out := &in.EffectiveSchedules, &out.EffectiveSchedules
-		*out = make(map[JobType]string, len(*in))
+		*out = make(map[JobType]ScheduleDefinition, len(*in))
 		for key, val := range *in {
 			(*out)[key] = val
 		}
diff --git a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_archives.yaml b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_archives.yaml
index 1c6fbadc..41e7f9bb 100644
--- a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_archives.yaml
+++ b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_archives.yaml
@@ -33,7 +33,7 @@ spec:
             description: ArchiveSpec defines the desired state of Archive.
             properties:
               backend:
-                description: Backend contains the backend information
+                description: Backend contains the restic repo where the job should backup to.
                 properties:
                   azure:
                     properties:
diff --git a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_checks.yaml b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_checks.yaml
index 2f65de97..2f8299dd 100644
--- a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_checks.yaml
+++ b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_checks.yaml
@@ -33,6 +33,7 @@ spec:
             description: CheckSpec defines the desired state of Check. It needs to contain the repository information.
             properties:
               backend:
+                description: Backend contains the restic repo where the job should backup to.
                 properties:
                   azure:
                     properties:
@@ -242,8 +243,10 @@ spec:
                     type: object
                 type: object
               keepJobs:
+                description: KeepJobs amount of jobs to keep for later analysis
                 type: integer
               promURL:
+                description: PromURL sets a prometheus push URL where the backup container send metrics to
                 type: string
               resources:
                 description: Resources describes the compute resource requirements (cpu, memory, etc.)
diff --git a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_prunes.yaml b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_prunes.yaml
index bcd6f7bf..bcb70ea8 100644
--- a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_prunes.yaml
+++ b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_prunes.yaml
@@ -33,6 +33,7 @@ spec:
             description: PruneSpec needs to contain the repository information as well as the desired retention policies.
             properties:
               backend:
+                description: Backend contains the restic repo where the job should backup to.
                 properties:
                   azure:
                     properties:
diff --git a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_restores.yaml b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_restores.yaml
index 2a612a52..e557147a 100644
--- a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_restores.yaml
+++ b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_restores.yaml
@@ -33,7 +33,7 @@ spec:
             description: RestoreSpec can either contain an S3 restore point or a local one. For the local one you need to define an existing PVC.
             properties:
               backend:
-                description: Backend contains the backend information
+                description: Backend contains the restic repo where the job should backup to.
                 properties:
                   azure:
                     properties:
diff --git a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_schedules.yaml b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_schedules.yaml
index d09844b8..3f8b7f93 100644
--- a/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_schedules.yaml
+++ b/config/crd/apiextensions.k8s.io/v1/backup.appuio.ch_schedules.yaml
@@ -36,7 +36,7 @@ spec:
                 description: ArchiveSchedule manages schedules for the archival service
                 properties:
                   backend:
-                    description: Backend contains the backend information
+                    description: Backend contains the restic repo where the job should backup to.
                     properties:
                       azure:
                         properties:
@@ -326,6 +326,7 @@ spec:
                         type: object
                     type: object
                   schedule:
+                    description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                     type: string
                   snapshot:
                     type: string
@@ -788,6 +789,7 @@ spec:
                         type: object
                     type: object
                   schedule:
+                    description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                     type: string
                   statsURL:
                     description: StatsURL sets an arbitrary URL where the wrestic container posts metrics and information about the snapshots to. This is in addition to the prometheus pushgateway.
@@ -802,6 +804,7 @@ spec:
                 description: CheckSchedule manages the schedules for the checks
                 properties:
                   backend:
+                    description: Backend contains the restic repo where the job should backup to.
                     properties:
                       azure:
                         properties:
@@ -1013,8 +1016,10 @@ spec:
                   concurrentRunsAllowed:
                     type: boolean
                   keepJobs:
+                    description: KeepJobs amount of jobs to keep for later analysis
                     type: integer
                   promURL:
+                    description: PromURL sets a prometheus push URL where the backup container send metrics to
                     type: string
                   resources:
                     description: Resources describes the compute resource requirements (cpu, memory, etc.)
@@ -1039,6 +1044,7 @@ spec:
                         type: object
                     type: object
                   schedule:
+                    description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                     type: string
                 type: object
               keepJobs:
@@ -1046,6 +1052,7 @@ spec:
               prune:
                 properties:
                   backend:
+                    description: Backend contains the restic repo where the job should backup to.
                     properties:
                       azure:
                         properties:
@@ -1311,6 +1318,7 @@ spec:
                         type: array
                     type: object
                   schedule:
+                    description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                     type: string
                 type: object
               resourceRequirementsTemplate:
@@ -1339,7 +1347,7 @@ spec:
                 description: RestoreSchedule manages schedules for the restore service
                 properties:
                   backend:
-                    description: Backend contains the backend information
+                    description: Backend contains the restic repo where the job should backup to.
                     properties:
                       azure:
                         properties:
@@ -1629,6 +1637,7 @@ spec:
                         type: object
                     type: object
                   schedule:
+                    description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                     type: string
                   snapshot:
                     type: string
@@ -1644,6 +1653,7 @@ spec:
             properties:
               effectiveSchedules:
                 additionalProperties:
+                  description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                   type: string
                 description: EffectiveSchedules displays the final schedule for each type (useful when using smart schedules).
                 type: object
diff --git a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_archives.yaml b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_archives.yaml
index f345261d..64fc9e30 100644
--- a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_archives.yaml
+++ b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_archives.yaml
@@ -33,7 +33,7 @@ spec:
           description: ArchiveSpec defines the desired state of Archive.
           properties:
             backend:
-              description: Backend contains the backend information
+              description: Backend contains the restic repo where the job should backup to.
               properties:
                 azure:
                   properties:
diff --git a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_checks.yaml b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_checks.yaml
index d8d0ce64..ed541b84 100644
--- a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_checks.yaml
+++ b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_checks.yaml
@@ -33,6 +33,7 @@ spec:
           description: CheckSpec defines the desired state of Check. It needs to contain the repository information.
           properties:
             backend:
+              description: Backend contains the restic repo where the job should backup to.
               properties:
                 azure:
                   properties:
@@ -242,8 +243,10 @@ spec:
                   type: object
               type: object
             keepJobs:
+              description: KeepJobs amount of jobs to keep for later analysis
               type: integer
             promURL:
+              description: PromURL sets a prometheus push URL where the backup container send metrics to
               type: string
             resources:
               description: Resources describes the compute resource requirements (cpu, memory, etc.)
diff --git a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_prunes.yaml b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_prunes.yaml
index a83f48e0..d3cf320c 100644
--- a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_prunes.yaml
+++ b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_prunes.yaml
@@ -33,6 +33,7 @@ spec:
           description: PruneSpec needs to contain the repository information as well as the desired retention policies.
           properties:
             backend:
+              description: Backend contains the restic repo where the job should backup to.
               properties:
                 azure:
                   properties:
diff --git a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_restores.yaml b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_restores.yaml
index 3071e351..0bc0e4fc 100644
--- a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_restores.yaml
+++ b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_restores.yaml
@@ -33,7 +33,7 @@ spec:
           description: RestoreSpec can either contain an S3 restore point or a local one. For the local one you need to define an existing PVC.
           properties:
             backend:
-              description: Backend contains the backend information
+              description: Backend contains the restic repo where the job should backup to.
               properties:
                 azure:
                   properties:
diff --git a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_schedules.yaml b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_schedules.yaml
index 74323ca9..cd0b7e6c 100644
--- a/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_schedules.yaml
+++ b/config/crd/apiextensions.k8s.io/v1beta1/backup.appuio.ch_schedules.yaml
@@ -36,7 +36,7 @@ spec:
               description: ArchiveSchedule manages schedules for the archival service
               properties:
                 backend:
-                  description: Backend contains the backend information
+                  description: Backend contains the restic repo where the job should backup to.
                   properties:
                     azure:
                       properties:
@@ -326,6 +326,7 @@ spec:
                       type: object
                   type: object
                 schedule:
+                  description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                   type: string
                 snapshot:
                   type: string
@@ -788,6 +789,7 @@ spec:
                       type: object
                   type: object
                 schedule:
+                  description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                   type: string
                 statsURL:
                   description: StatsURL sets an arbitrary URL where the wrestic container posts metrics and information about the snapshots to. This is in addition to the prometheus pushgateway.
@@ -802,6 +804,7 @@ spec:
               description: CheckSchedule manages the schedules for the checks
               properties:
                 backend:
+                  description: Backend contains the restic repo where the job should backup to.
                   properties:
                     azure:
                       properties:
@@ -1013,8 +1016,10 @@ spec:
                 concurrentRunsAllowed:
                   type: boolean
                 keepJobs:
+                  description: KeepJobs amount of jobs to keep for later analysis
                   type: integer
                 promURL:
+                  description: PromURL sets a prometheus push URL where the backup container send metrics to
                   type: string
                 resources:
                   description: Resources describes the compute resource requirements (cpu, memory, etc.)
@@ -1039,6 +1044,7 @@ spec:
                       type: object
                   type: object
                 schedule:
+                  description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                   type: string
               type: object
             keepJobs:
@@ -1046,6 +1052,7 @@ spec:
             prune:
               properties:
                 backend:
+                  description: Backend contains the restic repo where the job should backup to.
                   properties:
                     azure:
                       properties:
@@ -1311,6 +1318,7 @@ spec:
                       type: array
                   type: object
                 schedule:
+                  description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                   type: string
               type: object
             resourceRequirementsTemplate:
@@ -1339,7 +1347,7 @@ spec:
               description: RestoreSchedule manages schedules for the restore service
               properties:
                 backend:
-                  description: Backend contains the backend information
+                  description: Backend contains the restic repo where the job should backup to.
                   properties:
                     azure:
                       properties:
@@ -1629,6 +1637,7 @@ spec:
                       type: object
                   type: object
                 schedule:
+                  description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                   type: string
                 snapshot:
                   type: string
@@ -1644,6 +1653,7 @@ spec:
           properties:
             effectiveSchedules:
               additionalProperties:
+                description: ScheduleDefinition is the actual cron-type expression that defines the interval of the actions.
                 type: string
               description: EffectiveSchedules displays the final schedule for each type (useful when using smart schedules).
               type: object
diff --git a/docs/modules/ROOT/pages/references/schedule-specification.adoc b/docs/modules/ROOT/pages/references/schedule-specification.adoc
index 425e5533..454006c4 100644
--- a/docs/modules/ROOT/pages/references/schedule-specification.adoc
+++ b/docs/modules/ROOT/pages/references/schedule-specification.adoc
@@ -53,7 +53,7 @@ The following non-standard schedules are supported:
 |Run once a week, randomized start time and weekday
 |`52 4 * * 4` (example)
 
-|`@daily-random` or `@midnight`
+|`@daily-random`
 |Run once a day, randomized start time
 |`52 4 * * *` (example)
 
diff --git a/handler/randomizer.go b/handler/randomizer.go
index 18929843..0800a052 100644
--- a/handler/randomizer.go
+++ b/handler/randomizer.go
@@ -4,8 +4,9 @@ import (
 	"crypto/sha1"
 	"encoding/hex"
 	"fmt"
-	k8upv1alpha1 "github.com/vshn/k8up/api/v1alpha1"
 	"math/big"
+
+	k8upv1a1 "github.com/vshn/k8up/api/v1alpha1"
 )
 
 const (
@@ -17,13 +18,13 @@ const (
 	ScheduleWeeklyRandom   = "@weekly-random"
 )
 
-func (s *ScheduleHandler) createSeed(schedule *k8upv1alpha1.Schedule, jobType k8upv1alpha1.JobType) string {
-	return schedule.Namespace + "/" + schedule.Name + "@" + string(jobType)
+func (s *ScheduleHandler) createSeed(schedule *k8upv1a1.Schedule, jobType k8upv1a1.JobType) string {
+	return schedule.Namespace + "/" + schedule.Name + "@" + jobType.String()
 }
 
 // randomizeSchedule randomizes the given originalSchedule with a seed. The originalSchedule has to be one of the supported
 // '@x-random' predefined schedules, otherwise it returns an error with the original schedule unmodified.
-func randomizeSchedule(seed, originalSchedule string) (string, error) {
+func randomizeSchedule(seed string, originalSchedule k8upv1a1.ScheduleDefinition) (k8upv1a1.ScheduleDefinition, error) {
 	checksum := calculateChecksumFromSeed(seed)
 
 	minute := remainderFromModulo(checksum, 60, 0)
@@ -34,18 +35,18 @@ func randomizeSchedule(seed, originalSchedule string) (string, error) {
 
 	switch originalSchedule {
 	case ScheduleHourlyRandom:
-		return fmt.Sprintf("%d * * * *", minute), nil
+		return k8upv1a1.ScheduleDefinition(fmt.Sprintf("%d * * * *", minute)), nil
 	case ScheduleDailyRandom:
-		return fmt.Sprintf("%d %d * * *", minute, hour), nil
+		return k8upv1a1.ScheduleDefinition(fmt.Sprintf("%d %d * * *", minute, hour)), nil
 	case ScheduleMonthlyRandom:
-		return fmt.Sprintf("%d %d %d * *", minute, hour, dayOfMonth), nil
+		return k8upv1a1.ScheduleDefinition(fmt.Sprintf("%d %d %d * *", minute, hour, dayOfMonth)), nil
 	case ScheduleAnnuallyRandom:
 	case ScheduleYearlyRandom:
 		month := remainderFromModulo(checksum, 12, 1)
-		return fmt.Sprintf("%d %d %d %d *", minute, hour, dayOfMonth, month), nil
+		return k8upv1a1.ScheduleDefinition(fmt.Sprintf("%d %d %d %d *", minute, hour, dayOfMonth, month)), nil
 	case ScheduleWeeklyRandom:
 		weekday := remainderFromModulo(checksum, 6, 0)
-		return fmt.Sprintf("%d %d * * %d", minute, hour, weekday), nil
+		return k8upv1a1.ScheduleDefinition(fmt.Sprintf("%d %d * * %d", minute, hour, weekday)), nil
 	default:
 		return originalSchedule, fmt.Errorf("unrecognized random schedule: '%s'", originalSchedule)
 	}
diff --git a/handler/schedule.go b/handler/schedule.go
index 202929e2..c62e5008 100644
--- a/handler/schedule.go
+++ b/handler/schedule.go
@@ -2,10 +2,8 @@ package handler
 
 import (
 	"fmt"
-	"strings"
 
 	"github.com/imdario/mergo"
-	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -25,7 +23,6 @@ const (
 type ScheduleHandler struct {
 	schedule *k8upv1alpha1.Schedule
 	job.Config
-	requireSpecUpdate   bool
 	requireStatusUpdate bool
 }
 
@@ -62,80 +59,100 @@ func (s *ScheduleHandler) Handle() error {
 
 	if !controllerutil.ContainsFinalizer(s.schedule, scheduleFinalizerName) {
 		controllerutil.AddFinalizer(s.schedule, scheduleFinalizerName)
-		s.requireSpecUpdate = true
-	}
-
-	if s.requireSpecUpdate {
 		return s.updateSchedule()
 	}
+
 	if s.requireStatusUpdate {
 		return s.updateStatus()
 	}
 	return nil
 }
+
 func (s *ScheduleHandler) createJobList() scheduler.JobList {
 	jobList := scheduler.JobList{
 		Config: s.Config,
 		Jobs:   make([]scheduler.Job, 0),
 	}
 
-	if s.schedule.Spec.Archive != nil {
-		s.schedule.Spec.Archive.ArchiveSpec.Resources = s.mergeResourcesWithDefaults(s.schedule.Spec.Archive.Resources)
+	if archive := s.schedule.Spec.Archive; archive != nil {
+		jobTemplate := archive.DeepCopy()
+		s.mergeWithDefaults(&jobTemplate.RunnableSpec)
 		jobType := k8upv1alpha1.ArchiveType
 		jobList.Jobs = append(jobList.Jobs, scheduler.Job{
 			JobType:  jobType,
-			Schedule: s.getEffectiveSchedule(jobType, s.schedule.Spec.Archive.Schedule),
-			Object:   s.schedule.Spec.Archive.ArchiveSpec,
+			Schedule: s.getEffectiveSchedule(jobType, jobTemplate.Schedule),
+			Object:   jobTemplate.ArchiveSpec,
 		})
 	}
-	if s.schedule.Spec.Backup != nil {
-		s.schedule.Spec.Backup.BackupSpec.Resources = s.mergeResourcesWithDefaults(s.schedule.Spec.Backup.Resources)
+	if backup := s.schedule.Spec.Backup; backup != nil {
+		backupTemplate := backup.DeepCopy()
+		s.mergeWithDefaults(&backupTemplate.RunnableSpec)
 		jobType := k8upv1alpha1.BackupType
 		jobList.Jobs = append(jobList.Jobs, scheduler.Job{
 			JobType:  jobType,
-			Schedule: s.getEffectiveSchedule(k8upv1alpha1.BackupType, s.schedule.Spec.Backup.Schedule),
-			Object:   s.schedule.Spec.Backup.BackupSpec,
+			Schedule: s.getEffectiveSchedule(jobType, backupTemplate.Schedule),
+			Object:   backupTemplate.BackupSpec,
 		})
 	}
-	if s.schedule.Spec.Check != nil {
-		s.schedule.Spec.Check.CheckSpec.Resources = s.mergeResourcesWithDefaults(s.schedule.Spec.Check.Resources)
+	if check := s.schedule.Spec.Check; check != nil {
+		checkTemplate := check.DeepCopy()
+		s.mergeWithDefaults(&checkTemplate.RunnableSpec)
 		jobType := k8upv1alpha1.CheckType
 		jobList.Jobs = append(jobList.Jobs, scheduler.Job{
 			JobType:  jobType,
-			Schedule: s.getEffectiveSchedule(k8upv1alpha1.CheckType, s.schedule.Spec.Check.Schedule),
-			Object:   s.schedule.Spec.Check.CheckSpec,
+			Schedule: s.getEffectiveSchedule(jobType, checkTemplate.Schedule),
+			Object:   checkTemplate.CheckSpec,
 		})
 	}
-	if s.schedule.Spec.Restore != nil {
-		s.schedule.Spec.Restore.RestoreSpec.Resources = s.mergeResourcesWithDefaults(s.schedule.Spec.Restore.Resources)
+	if restore := s.schedule.Spec.Restore; restore != nil {
+		restoreTemplate := restore.DeepCopy()
+		s.mergeWithDefaults(&restoreTemplate.RunnableSpec)
 		jobType := k8upv1alpha1.RestoreType
 		jobList.Jobs = append(jobList.Jobs, scheduler.Job{
 			JobType:  jobType,
-			Schedule: s.getEffectiveSchedule(k8upv1alpha1.RestoreType, s.schedule.Spec.Restore.Schedule),
-			Object:   s.schedule.Spec.Restore.RestoreSpec,
+			Schedule: s.getEffectiveSchedule(jobType, restoreTemplate.Schedule),
+			Object:   restoreTemplate.RestoreSpec,
 		})
 	}
-	if s.schedule.Spec.Prune != nil {
-		s.schedule.Spec.Prune.PruneSpec.Resources = s.mergeResourcesWithDefaults(s.schedule.Spec.Prune.Resources)
+	if prune := s.schedule.Spec.Prune; prune != nil {
+		pruneTemplate := prune.DeepCopy()
+		s.mergeWithDefaults(&pruneTemplate.RunnableSpec)
 		jobType := k8upv1alpha1.PruneType
 		jobList.Jobs = append(jobList.Jobs, scheduler.Job{
 			JobType:  jobType,
-			Schedule: s.getEffectiveSchedule(k8upv1alpha1.PruneType, s.schedule.Spec.Prune.Schedule),
-			Object:   s.schedule.Spec.Prune.PruneSpec,
+			Schedule: s.getEffectiveSchedule(jobType, pruneTemplate.Schedule),
+			Object:   pruneTemplate.PruneSpec,
 		})
 	}
 
 	return jobList
 }
 
-func (s *ScheduleHandler) mergeResourcesWithDefaults(resources corev1.ResourceRequirements) corev1.ResourceRequirements {
+func (s *ScheduleHandler) mergeWithDefaults(specInstance *k8upv1alpha1.RunnableSpec) {
+	s.mergeResourcesWithDefaults(specInstance)
+	s.mergeBackendWithDefaults(specInstance)
+}
+
+func (s *ScheduleHandler) mergeResourcesWithDefaults(specInstance *k8upv1alpha1.RunnableSpec) {
+	resources := &specInstance.Resources
+
 	if err := mergo.Merge(&s.schedule.Spec.ResourceRequirementsTemplate, cfg.Config.GetGlobalDefaultResources()); err != nil {
-		s.Log.Info("could not merge specific resources with global defaults", "err", err.Error())
+		s.Log.Info("could not merge specific resources with global defaults", "err", err.Error(), "schedule", s.Obj.GetMetaObject().GetName(), "namespace", s.Obj.GetMetaObject().GetNamespace())
+	}
+	if err := mergo.Merge(resources, s.schedule.Spec.ResourceRequirementsTemplate); err != nil {
+		s.Log.Info("could not merge specific resources with schedule defaults", "err", err.Error(), "schedule", s.Obj.GetMetaObject().GetName(), "namespace", s.Obj.GetMetaObject().GetNamespace())
+	}
+}
+
+func (s *ScheduleHandler) mergeBackendWithDefaults(specInstance *k8upv1alpha1.RunnableSpec) {
+	if specInstance.Backend == nil {
+		specInstance.Backend = s.schedule.Spec.Backend.DeepCopy()
+		return
 	}
-	if err := mergo.Merge(&resources, s.schedule.Spec.ResourceRequirementsTemplate); err != nil {
-		s.Log.Info("could not merge specific resources with schedule defaults", "err", err.Error())
+
+	if err := mergo.Merge(specInstance.Backend, s.schedule.Spec.Backend); err != nil {
+		s.Log.Info("could not merge the schedule's backend with the resource's backend", "err", err.Error(), "schedule", s.Obj.GetMetaObject().GetName(), "namespace", s.Obj.GetMetaObject().GetNamespace())
 	}
-	return resources
 }
 
 func (s *ScheduleHandler) updateSchedule() error {
@@ -155,25 +172,40 @@ func (s *ScheduleHandler) updateStatus() error {
 	return nil
 }
 
-func (s *ScheduleHandler) getEffectiveSchedule(jobType k8upv1alpha1.JobType, originalSchedule string) string {
+func (s *ScheduleHandler) getEffectiveSchedule(jobType k8upv1alpha1.JobType, originalSchedule k8upv1alpha1.ScheduleDefinition) k8upv1alpha1.ScheduleDefinition {
 	if s.schedule.Status.EffectiveSchedules == nil {
-		s.schedule.Status.EffectiveSchedules = make(map[k8upv1alpha1.JobType]string)
+		s.schedule.Status.EffectiveSchedules = make(map[k8upv1alpha1.JobType]k8upv1alpha1.ScheduleDefinition)
 	}
 	if existingSchedule, found := s.schedule.Status.EffectiveSchedules[jobType]; found {
 		return existingSchedule
 	}
-	if !strings.HasSuffix(originalSchedule, "-random") {
+
+	isStandardOrNotRandom := !originalSchedule.IsNonStandard() || !originalSchedule.IsRandom()
+	if isStandardOrNotRandom {
+		return originalSchedule
+	}
+
+	randomizedSchedule, err := s.getRandomSchedule(jobType, originalSchedule)
+	if err != nil {
+		s.Log.Info("Could not randomize schedule, continuing with original schedule", "schedule", originalSchedule, "error", err.Error())
 		return originalSchedule
 	}
-	schedule := originalSchedule
+	s.setEffectiveSchedule(jobType, randomizedSchedule)
+	return randomizedSchedule
+}
+
+func (s *ScheduleHandler) getRandomSchedule(jobType k8upv1alpha1.JobType, originalSchedule k8upv1alpha1.ScheduleDefinition) (k8upv1alpha1.ScheduleDefinition, error) {
 	seed := s.createSeed(s.schedule, jobType)
-	schedule, err := randomizeSchedule(seed, originalSchedule)
+	randomizedSchedule, err := randomizeSchedule(seed, originalSchedule)
 	if err != nil {
-		s.Log.Info("Could not randomize schedule, ignoring and try original schedule", "error", err.Error())
-	} else {
-		s.Log.V(1).Info("Randomized schedule", "seed", seed, "from_schedule", originalSchedule, "effective_schedule", schedule)
+		return originalSchedule, err
 	}
+
+	s.Log.V(1).Info("Randomized schedule", "seed", seed, "from_schedule", originalSchedule, "effective_schedule", randomizedSchedule)
+	return randomizedSchedule, nil
+}
+
+func (s *ScheduleHandler) setEffectiveSchedule(jobType k8upv1alpha1.JobType, schedule k8upv1alpha1.ScheduleDefinition) {
 	s.schedule.Status.EffectiveSchedules[jobType] = schedule
 	s.requireStatusUpdate = true
-	return schedule
 }
diff --git a/queue/execution.go b/queue/execution.go
index 7a04a4b6..7a98eb2e 100644
--- a/queue/execution.go
+++ b/queue/execution.go
@@ -117,9 +117,9 @@ func (eq *ExecutionQueue) GetRepositories() []string {
 }
 
 func (eq *ExecutionQueue) incQueueGauge(namespace string, jobType v1alpha1.JobType) {
-	queueGauge.WithLabelValues(namespace, string(jobType)).Inc()
+	queueGauge.WithLabelValues(namespace, jobType.String()).Inc()
 }
 
 func (eq *ExecutionQueue) decQueueGauge(namespace string, jobType v1alpha1.JobType) {
-	queueGauge.WithLabelValues(namespace, string(jobType)).Dec()
+	queueGauge.WithLabelValues(namespace, jobType.String()).Dec()
 }
diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go
index 87aefe36..01a2fa29 100644
--- a/scheduler/scheduler.go
+++ b/scheduler/scheduler.go
@@ -33,7 +33,7 @@ type (
 	// Job contains all necessary information to create a schedule.
 	Job struct {
 		JobType  k8upv1alpha1.JobType
-		Schedule string
+		Schedule k8upv1alpha1.ScheduleDefinition
 		Object   ObjectCreator
 	}
 	// Scheduler handles all the schedules.
@@ -45,7 +45,7 @@ type (
 	scheduleRef struct {
 		EntryID  cron.EntryID
 		JobType  k8upv1alpha1.JobType
-		Schedule string
+		Schedule k8upv1alpha1.ScheduleDefinition
 		Command  func()
 	}
 )
@@ -86,7 +86,6 @@ func newScheduler() *Scheduler {
 
 // SyncSchedules will add the given schedule to the running cron.
 func (s *Scheduler) SyncSchedules(jobs JobList) error {
-
 	namespacedName := types.NamespacedName{
 		Name:      jobs.Config.Obj.GetMetaObject().GetName(),
 		Namespace: jobs.Config.Obj.GetMetaObject().GetNamespace(),
@@ -95,12 +94,13 @@ func (s *Scheduler) SyncSchedules(jobs JobList) error {
 	s.RemoveSchedules(namespacedName)
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
-	for _, jb := range jobs.Jobs {
-		jobs.Config.Log.Info("registering schedule for", "type", jb.JobType, "schedule", jb.Schedule)
-		if err := s.addSchedule(jb, namespacedName, func() {
-			jobs.Config.Log.Info("running schedule for", "jb", jb.JobType)
-			s.createObject(jb.JobType, namespacedName.Namespace, jb.Object, jobs.Config)
-		}); err != nil {
+	for _, schedulableJob := range jobs.Jobs {
+		jobs.Config.Log.Info("registering schedule for", "type", schedulableJob.JobType, "schedule", schedulableJob.Schedule)
+		err := s.addSchedule(schedulableJob, namespacedName, func() {
+			jobs.Config.Log.Info("running schedule for", "job", schedulableJob.JobType)
+			s.createObject(schedulableJob.JobType, namespacedName.Namespace, schedulableJob.Object, jobs.Config)
+		})
+		if err != nil {
 			return err
 		}
 	}
@@ -111,7 +111,7 @@ func (s *Scheduler) SyncSchedules(jobs JobList) error {
 
 // addSchedule adds the given newJobs to the cron scheduler
 func (s *Scheduler) addSchedule(jb Job, namespacedName types.NamespacedName, cmd func()) error {
-	id, err := s.cron.AddFunc(jb.Schedule, cmd)
+	id, err := s.cron.AddFunc(jb.Schedule.String(), cmd)
 	if err != nil {
 		return err
 	}


Test patch:
diff --git a/executor/restore_test.go b/executor/restore_test.go
index 9b9b84bd..e781062f 100644
--- a/executor/restore_test.go
+++ b/executor/restore_test.go
@@ -71,15 +71,17 @@ var _ = Describe("Restore", func() {
 							},
 						},
 					},
-					Backend: &k8upv1alpha1.Backend{
-						S3: &k8upv1alpha1.S3Spec{
-							Endpoint: "http://localhost:9000",
-							Bucket:   "test-backend",
-							AccessKeyIDSecretRef: &v1.SecretKeySelector{
-								Key: "accessKey-backend",
-							},
-							SecretAccessKeySecretRef: &v1.SecretKeySelector{
-								Key: "secretKey-backend",
+					RunnableSpec: k8upv1alpha1.RunnableSpec{
+						Backend: &k8upv1alpha1.Backend{
+							S3: &k8upv1alpha1.S3Spec{
+								Endpoint: "http://localhost:9000",
+								Bucket:   "test-backend",
+								AccessKeyIDSecretRef: &v1.SecretKeySelector{
+									Key: "accessKey-backend",
+								},
+								SecretAccessKeySecretRef: &v1.SecretKeySelector{
+									Key: "secretKey-backend",
+								},
 							},
 						},
 					},
diff --git a/handler/randomizer_test.go b/handler/randomizer_test.go
index 16bf0246..00921b84 100644
--- a/handler/randomizer_test.go
+++ b/handler/randomizer_test.go
@@ -1,10 +1,13 @@
 package handler
 
 import (
-	"github.com/stretchr/testify/assert"
 	"strconv"
 	"strings"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/vshn/k8up/api/v1alpha1"
 )
 
 func Test_randomizeSchedule_VerifyCronSyntax(t *testing.T) {
@@ -15,7 +18,7 @@ func Test_randomizeSchedule_VerifyCronSyntax(t *testing.T) {
 			seed := "namespace/name-" + strconv.Itoa(i) + "@backup"
 			schedule, err := randomizeSchedule(seed, "@yearly-random")
 			assert.NoError(t, err)
-			fields := strings.Split(schedule, " ")
+			fields := strings.Split(schedule.String(), " ")
 			for j, f := range fields {
 				number, _ := strconv.Atoi(f)
 				arr[j] = number
@@ -31,7 +34,7 @@ func Test_randomizeSchedule_VerifyCronSyntax(t *testing.T) {
 			seed := "namespace/name-" + strconv.Itoa(i) + "@backup"
 			schedule, err := randomizeSchedule(seed, "@weekly-random")
 			assert.NoError(t, err)
-			fields := strings.Split(schedule, " ")
+			fields := strings.Split(schedule.String(), " ")
 			for j, f := range fields {
 				number, _ := strconv.Atoi(f)
 				arr[j] = number
@@ -46,8 +49,8 @@ func Test_randomizeSchedule_VerifyCronSyntax(t *testing.T) {
 func Test_randomizeSchedule_VerifySchedules(t *testing.T) {
 	seed := "k8up-system/my-scheduled-backup@backup"
 	tests := map[string]struct {
-		schedule         string
-		expectedSchedule string
+		schedule         v1alpha1.ScheduleDefinition
+		expectedSchedule v1alpha1.ScheduleDefinition
 	}{
 		"WhenScheduleRandomHourlyGiven_ThenReturnStableRandomizedSchedule": {
 			schedule:         "@hourly-random",
diff --git a/handler/schedule_test.go b/handler/schedule_test.go
index e66a9d65..439aa742 100644
--- a/handler/schedule_test.go
+++ b/handler/schedule_test.go
@@ -1,15 +1,17 @@
 package handler
 
 import (
+	"testing"
+
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"github.com/vshn/k8up/api/v1alpha1"
-	"github.com/vshn/k8up/cfg"
-	"github.com/vshn/k8up/job"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
-	"testing"
+
+	"github.com/vshn/k8up/api/v1alpha1"
+	"github.com/vshn/k8up/cfg"
+	"github.com/vshn/k8up/job"
 )
 
 func TestScheduleHandler_mergeResourcesWithDefaults(t *testing.T) {
@@ -147,8 +149,9 @@ func TestScheduleHandler_mergeResourcesWithDefaults(t *testing.T) {
 			s := ScheduleHandler{schedule: &v1alpha1.Schedule{Spec: v1alpha1.ScheduleSpec{
 				ResourceRequirementsTemplate: tt.template,
 			}}}
-			result := s.mergeResourcesWithDefaults(tt.resources)
-			assert.Equal(t, tt.expected, result)
+			spec := &v1alpha1.RunnableSpec{Resources: tt.resources}
+			s.mergeResourcesWithDefaults(spec)
+			assert.Equal(t, tt.expected, spec.Resources)
 		})
 	}
 }
@@ -156,9 +159,9 @@ func TestScheduleHandler_mergeResourcesWithDefaults(t *testing.T) {
 func TestScheduleHandler_getEffectiveSchedule(t *testing.T) {
 	tests := map[string]struct {
 		schedule             *v1alpha1.Schedule
-		originalSchedule     string
+		originalSchedule     v1alpha1.ScheduleDefinition
 		expectedStatusUpdate bool
-		expectedSchedule     string
+		expectedSchedule     v1alpha1.ScheduleDefinition
 	}{
 		"GivenScheduleWithoutStatus_WhenUsingRandomSchedule_ThenPutGeneratedScheduleInStatus": {
 			schedule: &v1alpha1.Schedule{
@@ -176,7 +179,7 @@ func TestScheduleHandler_getEffectiveSchedule(t *testing.T) {
 					Backup: &v1alpha1.BackupSchedule{},
 				},
 				Status: v1alpha1.ScheduleStatus{
-					EffectiveSchedules: map[v1alpha1.JobType]string{
+					EffectiveSchedules: map[v1alpha1.JobType]v1alpha1.ScheduleDefinition{
 						v1alpha1.BackupType: "26 * 3 * *",
 					},
 				},