diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a40b2d7e8..fc7f89322 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,6 +11,7 @@ `bug`, `enhancement`, `documentation`, `change`, `breaking`, `dependency` as they show up in the changelog - [ ] PR contains the label `area:operator` +- [ ] Commits are [signed off](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) - [ ] Link this PR to related issues - [ ] I have not made _any_ changes in the `charts/` directory. @@ -21,6 +22,7 @@ as they show up in the changelog - [ ] PR contains the label `area:chart` - [ ] PR contains the chart label, e.g. `chart:k8up` +- [ ] Commits are [signed off](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) - [ ] Variables are documented in the values.yaml using the format required by [Helm-Docs](https://github.com/norwoodj/helm-docs#valuesyaml-metadata). - [ ] Chart Version bumped if immediate release after merging is planned - [ ] I have run `make chart-docs` diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da3bd69a7..47126912f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,4 +35,7 @@ jobs: ${{ runner.os }}-go- - name: Run tests + run: make test + + - name: Run integration tests run: make integration-test diff --git a/Makefile b/Makefile index 5ac35a5fb..f4629fb0c 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ test: ## Run tests build: generate fmt vet $(BIN_FILENAME) docs-update-usage ## Build manager binary .PHONY: run +run: export ARGS := $(ARGS) operator run: export BACKUP_ENABLE_LEADER_ELECTION = $(ENABLE_LEADER_ELECTION) run: export K8UP_DEBUG = true run: export BACKUP_OPERATOR_NAMESPACE = default diff --git a/api/v1/archive_types.go b/api/v1/archive_types.go index 6a1411b35..e6c68d4f9 100644 --- a/api/v1/archive_types.go +++ b/api/v1/archive_types.go @@ -1,10 +1,12 @@ package v1 import ( + "context" "reflect" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) // ArchiveSpec defines the desired state of Archive. @@ -87,6 +89,13 @@ func (a *Archive) GetSuccessfulJobsHistoryLimit() *int { return a.Spec.KeepJobs } +func (a *Archive) GetPodConfig(ctx context.Context, c client.Client) (*PodConfig, error) { + if a.Spec.RunnableSpec.PodConfigRef == nil { + return nil, nil + } + return NewPodConfig(ctx, a.Spec.RunnableSpec.PodConfigRef.Name, a.GetNamespace(), c) +} + // GetJobObjects returns a sortable list of jobs func (a *ArchiveList) GetJobObjects() JobObjectList { items := make(JobObjectList, len(a.Items)) diff --git a/api/v1/backup_types.go b/api/v1/backup_types.go index 998ad509e..61b40a575 100644 --- a/api/v1/backup_types.go +++ b/api/v1/backup_types.go @@ -1,10 +1,12 @@ package v1 import ( + "context" "reflect" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) // BackupSpec defines a single backup. It must contain all information to connect to @@ -127,6 +129,13 @@ func (b *Backup) GetSuccessfulJobsHistoryLimit() *int { return b.Spec.KeepJobs } +func (b *Backup) GetPodConfig(ctx context.Context, c client.Client) (*PodConfig, error) { + if b.Spec.RunnableSpec.PodConfigRef == nil { + return nil, nil + } + return NewPodConfig(ctx, b.Spec.RunnableSpec.PodConfigRef.Name, b.GetNamespace(), c) +} + // GetJobObjects returns a sortable list of jobs func (b *BackupList) GetJobObjects() JobObjectList { items := make(JobObjectList, len(b.Items)) diff --git a/api/v1/check_types.go b/api/v1/check_types.go index a93e4bde5..ffa3379a3 100644 --- a/api/v1/check_types.go +++ b/api/v1/check_types.go @@ -1,10 +1,12 @@ package v1 import ( + "context" "reflect" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) // CheckSpec defines the desired state of Check. It needs to contain the repository @@ -106,6 +108,13 @@ func (c *Check) GetSuccessfulJobsHistoryLimit() *int { return c.Spec.KeepJobs } +func (b *Check) GetPodConfig(ctx context.Context, c client.Client) (*PodConfig, error) { + if b.Spec.RunnableSpec.PodConfigRef == nil { + return nil, nil + } + return NewPodConfig(ctx, b.Spec.RunnableSpec.PodConfigRef.Name, b.GetNamespace(), c) +} + // GetJobObjects returns a sortable list of jobs func (c *CheckList) GetJobObjects() JobObjectList { items := make(JobObjectList, len(c.Items)) diff --git a/api/v1/job_object.go b/api/v1/job_object.go index 194a2544c..4de1a4e2a 100644 --- a/api/v1/job_object.go +++ b/api/v1/job_object.go @@ -1,6 +1,8 @@ package v1 import ( + "context" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -19,6 +21,8 @@ type JobObject interface { GetPodSecurityContext() *corev1.PodSecurityContext // GetActiveDeadlineSeconds returns the specified active deadline seconds timeout. GetActiveDeadlineSeconds() *int64 + // GetPodConfig returns the defined PodSpec + GetPodConfig(context.Context, client.Client) (*PodConfig, error) } // +k8s:deepcopy-gen=false diff --git a/api/v1/podconfig_types.go b/api/v1/podconfig_types.go new file mode 100644 index 000000000..8a9e5d0d3 --- /dev/null +++ b/api/v1/podconfig_types.go @@ -0,0 +1,60 @@ +package v1 + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// +kubebuilder:rbac:groups=k8up.io,resources=podconfigs,verbs=get;list;watch + +// PodConfigSpec contains the podTemplate definition. +type PodConfigSpec struct { + Template corev1.PodTemplateSpec `json:"template,omitempty"` +} + +// PodConfigStatus defines the observed state of Snapshot +type PodConfigStatus struct { +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// PodConfig is the Schema for the PodConcig API +// Any annotations and labels set on this object will also be set on +// the final pod. +type PodConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PodConfigSpec `json:"spec,omitempty"` + Status PodConfigStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// SnapshotList contains a list of Snapshot +type PodConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PodConfig `json:"items"` +} + +func NewPodConfig(ctx context.Context, name, namespace string, c client.Client) (*PodConfig, error) { + config := &PodConfig{} + err := c.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, config) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + return config, nil +} + +func init() { + SchemeBuilder.Register(&PodConfig{}, &PodConfigList{}) +} diff --git a/api/v1/prune_types.go b/api/v1/prune_types.go index fb1fed0ac..31c452e0d 100644 --- a/api/v1/prune_types.go +++ b/api/v1/prune_types.go @@ -1,10 +1,12 @@ package v1 import ( + "context" "reflect" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) // PruneSpec needs to contain the repository information as well as the desired @@ -115,6 +117,13 @@ func (p *Prune) GetSuccessfulJobsHistoryLimit() *int { return p.Spec.KeepJobs } +func (p *Prune) GetPodConfig(ctx context.Context, c client.Client) (*PodConfig, error) { + if p.Spec.RunnableSpec.PodConfigRef == nil { + return nil, nil + } + return NewPodConfig(ctx, p.Spec.RunnableSpec.PodConfigRef.Name, p.GetNamespace(), c) +} + // GetJobObjects returns a sortable list of jobs func (p *PruneList) GetJobObjects() JobObjectList { items := make(JobObjectList, len(p.Items)) diff --git a/api/v1/restore_types.go b/api/v1/restore_types.go index 231580ccd..fbfb72754 100644 --- a/api/v1/restore_types.go +++ b/api/v1/restore_types.go @@ -1,10 +1,12 @@ package v1 import ( + "context" "reflect" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) // RestoreSpec can either contain an S3 restore point or a local one. For the local @@ -107,6 +109,13 @@ func (r *Restore) GetSuccessfulJobsHistoryLimit() *int { return r.Spec.KeepJobs } +func (r *Restore) GetPodConfig(ctx context.Context, c client.Client) (*PodConfig, error) { + if r.Spec.RunnableSpec.PodConfigRef == nil { + return nil, nil + } + return NewPodConfig(ctx, r.Spec.RunnableSpec.PodConfigRef.Name, r.GetNamespace(), c) +} + // GetJobObjects returns a sortable list of jobs func (r *RestoreList) GetJobObjects() JobObjectList { items := make(JobObjectList, len(r.Items)) diff --git a/api/v1/runnable_types.go b/api/v1/runnable_types.go index 4ac1a8001..cc15cde76 100644 --- a/api/v1/runnable_types.go +++ b/api/v1/runnable_types.go @@ -15,6 +15,12 @@ type RunnableSpec struct { // PodSecurityContext describes the security context with which this action shall be executed. PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` + // PodConfigRef describes the pod spec with wich this action shall be executed. + // It takes precedence over the Resources or PodSecurityContext field. + // It does not allow changing the image or the command of the resulting pod. + // This is for advanced use-cases only. Please only set this if you know what you're doing. + PodConfigRef *corev1.LocalObjectReference `json:"podConfigRef,omitempty"` + // Volumes List of volumes that can be mounted by containers belonging to the pod. Volumes *[]RunnableVolumeSpec `json:"volumes,omitempty"` diff --git a/api/v1/schedule_types.go b/api/v1/schedule_types.go index cd77398c8..54fe13002 100644 --- a/api/v1/schedule_types.go +++ b/api/v1/schedule_types.go @@ -1,11 +1,13 @@ package v1 import ( + "context" "strings" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) // ScheduleSpec defines the schedules for the various job types. @@ -36,6 +38,10 @@ type ScheduleSpec struct { // PodSecurityContext describes the security context with which actions (such as backups) shall be executed. PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` + + // PodConfigRef will apply the given template to all job definitions in this Schedule. + // It can be overriden for specific jobs if necessary. + PodConfigRef *corev1.LocalObjectReference `json:"podConfigRef,omitempty"` } // ScheduleDefinition is the actual cron-type expression that defines the interval of the actions. @@ -185,6 +191,13 @@ func (s *Schedule) GetSuccessfulJobsHistoryLimit() *int { return s.Spec.KeepJobs } +func (s *Schedule) GetPodConfig(ctx context.Context, c client.Client) (*PodConfig, error) { + if s.Spec.PodConfigRef == nil { + return nil, nil + } + return NewPodConfig(ctx, s.Spec.PodConfigRef.Name, s.GetNamespace(), c) +} + // String casts the value to string. // "aScheduleDefinition.String()" and "string(aScheduleDefinition)" are equivalent. func (s ScheduleDefinition) String() string { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 547ee7bf5..85c50f571 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -597,6 +597,96 @@ func (in *Pod) DeepCopy() *Pod { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodConfig) DeepCopyInto(out *PodConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodConfig. +func (in *PodConfig) DeepCopy() *PodConfig { + if in == nil { + return nil + } + out := new(PodConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PodConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodConfigList) DeepCopyInto(out *PodConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PodConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodConfigList. +func (in *PodConfigList) DeepCopy() *PodConfigList { + if in == nil { + return nil + } + out := new(PodConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PodConfigList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodConfigSpec) DeepCopyInto(out *PodConfigSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodConfigSpec. +func (in *PodConfigSpec) DeepCopy() *PodConfigSpec { + if in == nil { + return nil + } + out := new(PodConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodConfigStatus) DeepCopyInto(out *PodConfigStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodConfigStatus. +func (in *PodConfigStatus) DeepCopy() *PodConfigStatus { + if in == nil { + return nil + } + out := new(PodConfigStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PreBackupPod) DeepCopyInto(out *PreBackupPod) { *out = *in @@ -1018,6 +1108,11 @@ func (in *RunnableSpec) DeepCopyInto(out *RunnableSpec) { *out = new(corev1.PodSecurityContext) (*in).DeepCopyInto(*out) } + if in.PodConfigRef != nil { + in, out := &in.PodConfigRef, &out.PodConfigRef + *out = new(corev1.LocalObjectReference) + **out = **in + } if in.Volumes != nil { in, out := &in.Volumes, &out.Volumes *out = new([]RunnableVolumeSpec) @@ -1229,6 +1324,11 @@ func (in *ScheduleSpec) DeepCopyInto(out *ScheduleSpec) { *out = new(corev1.PodSecurityContext) (*in).DeepCopyInto(*out) } + if in.PodConfigRef != nil { + in, out := &in.PodConfigRef, &out.PodConfigRef + *out = new(corev1.LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScheduleSpec. diff --git a/charts/k8up/Chart.yaml b/charts/k8up/Chart.yaml index 5ba505310..98fea299f 100644 --- a/charts/k8up/Chart.yaml +++ b/charts/k8up/Chart.yaml @@ -6,7 +6,7 @@ keywords: - backup - operator - restic -version: 4.5.1 +version: 4.7.0 sources: - https://github.com/k8up-io/k8up maintainers: diff --git a/charts/k8up/README.md b/charts/k8up/README.md index 4b980e52a..fcec49f73 100644 --- a/charts/k8up/README.md +++ b/charts/k8up/README.md @@ -1,6 +1,6 @@ # k8up -![Version: 4.5.1](https://img.shields.io/badge/Version-4.5.1-informational?style=flat-square) +![Version: 4.7.0](https://img.shields.io/badge/Version-4.7.0-informational?style=flat-square) Kubernetes and OpenShift Backup Operator based on restic @@ -13,7 +13,7 @@ helm repo add k8up-io https://k8up-io.github.io/k8up helm install k8up k8up-io/k8up ``` ```bash -kubectl apply -f https://github.com/k8up-io/k8up/releases/download/k8up-4.5.1/k8up-crd.yaml +kubectl apply -f https://github.com/k8up-io/k8up/releases/download/k8up-4.7.0/k8up-crd.yaml ```