mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-14 02:48:57 +00:00
Compare commits
69 Commits
feat/expos
...
backport-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e63ed41fee | ||
|
|
5b613836ee | ||
|
|
16d82b13ad | ||
|
|
deb8d22bc8 | ||
|
|
6cd06ced9a | ||
|
|
64b7984e51 | ||
|
|
f214f2983b | ||
|
|
1ca75ee10d | ||
|
|
8dc1d53874 | ||
|
|
789aca4c84 | ||
|
|
8b7bc9c1e1 | ||
|
|
17966bfba1 | ||
|
|
a9de6bf07e | ||
|
|
765cffdc66 | ||
|
|
48ebe418b2 | ||
|
|
25f31022f0 | ||
|
|
5be383610f | ||
|
|
363c8cf066 | ||
|
|
8c8ab1f1a0 | ||
|
|
9405e05185 | ||
|
|
0aee5b4dfa | ||
|
|
8373a8cb3f | ||
|
|
cb0e457dda | ||
|
|
c74b88ce66 | ||
|
|
07982b2646 | ||
|
|
d80ea29dfa | ||
|
|
143da399ff | ||
|
|
ad3bff6692 | ||
|
|
f1852ca841 | ||
|
|
5914515371 | ||
|
|
5be4fc8f40 | ||
|
|
f4ff44a18c | ||
|
|
6b5e2e3e77 | ||
|
|
fc84b80b70 | ||
|
|
85a0003ffb | ||
|
|
7c766fc27d | ||
|
|
46d81c806f | ||
|
|
9300309dfb | ||
|
|
d90a76bc25 | ||
|
|
13bfb72ea5 | ||
|
|
56ed29c7fc | ||
|
|
1ffa1ef454 | ||
|
|
1826858bb0 | ||
|
|
0e262838d2 | ||
|
|
b610630592 | ||
|
|
357d9f95be | ||
|
|
37a2e38798 | ||
|
|
da44a65ba9 | ||
|
|
26da347de8 | ||
|
|
dd2798dbda | ||
|
|
fb5b4da2b2 | ||
|
|
540e1c6e0d | ||
|
|
1dd8e00e17 | ||
|
|
79165ca2f9 | ||
|
|
a8bd179f0d | ||
|
|
1491535b35 | ||
|
|
290c6be04b | ||
|
|
03328dc4e4 | ||
|
|
9311a9e547 | ||
|
|
548b2c0ed3 | ||
|
|
202ff3433e | ||
|
|
891195018f | ||
|
|
d53861837f | ||
|
|
f1a75ab864 | ||
|
|
2110534e63 | ||
|
|
2e22a6579e | ||
|
|
35907dd474 | ||
|
|
af56c105c2 | ||
|
|
30e5b71e3f |
3
Makefile
3
Makefile
@@ -15,9 +15,10 @@ build: build-deps
|
||||
make -C packages/extra/monitoring image
|
||||
make -C packages/system/cozystack-api image
|
||||
make -C packages/system/cozystack-controller image
|
||||
make -C packages/system/backup-controller image
|
||||
make -C packages/system/lineage-controller-webhook image
|
||||
make -C packages/system/cilium image
|
||||
make -C packages/system/kubeovn image
|
||||
make -C packages/system/linstor image
|
||||
make -C packages/system/kubeovn-webhook image
|
||||
make -C packages/system/kubeovn-plunger image
|
||||
make -C packages/system/dashboard image
|
||||
|
||||
1
api/.gitattributes
vendored
Normal file
1
api/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
zz_generated_deepcopy.go linguist-generated
|
||||
37
api/backups/strategy/v1alpha1/groupversion_info.go
Normal file
37
api/backups/strategy/v1alpha1/groupversion_info.go
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package v1alpha1 contains API Schema definitions for the v1alpha1 API group.
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=strategy.backups.cozystack.io
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
GroupVersion = schema.GroupVersion{Group: "strategy.backups.cozystack.io", Version: "v1alpha1"}
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addGroupVersion)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func addGroupVersion(scheme *runtime.Scheme) error {
|
||||
metav1.AddToGroupVersion(scheme, GroupVersion)
|
||||
return nil
|
||||
}
|
||||
63
api/backups/strategy/v1alpha1/job_types.go
Normal file
63
api/backups/strategy/v1alpha1/job_types.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines strategy.backups.cozystack.io API types.
|
||||
//
|
||||
// Group: strategy.backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&Job{},
|
||||
&JobList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
JobStrategyKind = "Job"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
|
||||
// Job defines a backup strategy using a one-shot Job
|
||||
type Job struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec JobSpec `json:"spec,omitempty"`
|
||||
Status JobStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// JobList contains a list of backup Jobs.
|
||||
type JobList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Job `json:"items"`
|
||||
}
|
||||
|
||||
// JobSpec specifies the desired behavior of a backup job.
|
||||
type JobSpec struct {
|
||||
// Template holds a PodTemplateSpec with the right shape to
|
||||
// run a single pod to completion and create a tarball with
|
||||
// a given apps data. Helm-like Go templates are supported.
|
||||
// The values of the source application are available under
|
||||
// `.Values`. `.Release.Name` and `.Release.Namespace` are
|
||||
// also exported.
|
||||
Template corev1.PodTemplateSpec `json:"template"`
|
||||
}
|
||||
|
||||
type JobStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
64
api/backups/strategy/v1alpha1/velero_types.go
Normal file
64
api/backups/strategy/v1alpha1/velero_types.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines strategy.backups.cozystack.io API types.
|
||||
//
|
||||
// Group: strategy.backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&Velero{},
|
||||
&VeleroList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
VeleroStrategyKind = "Velero"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
|
||||
// Velero defines a backup strategy using Velero as the driver.
|
||||
type Velero struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec VeleroSpec `json:"spec,omitempty"`
|
||||
Status VeleroStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// VeleroList contains a list of Velero backup strategies.
|
||||
type VeleroList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Velero `json:"items"`
|
||||
}
|
||||
|
||||
// VeleroSpec specifies the desired strategy for backing up with Velero.
|
||||
type VeleroSpec struct {
|
||||
Template VeleroTemplate `json:"template"`
|
||||
}
|
||||
|
||||
// VeleroTemplate describes the data a backup.velero.io should have when
|
||||
// templated from a Velero backup strategy.
|
||||
type VeleroTemplate struct {
|
||||
Spec velerov1.BackupSpec `json:"spec"`
|
||||
RestoreSpec velerov1.RestoreSpec `json:"restoreSpec"`
|
||||
}
|
||||
|
||||
type VeleroStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
237
api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
Normal file
237
api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,237 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Job) DeepCopyInto(out *Job) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Job.
|
||||
func (in *Job) DeepCopy() *Job {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Job)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Job) 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 *JobList) DeepCopyInto(out *JobList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Job, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobList.
|
||||
func (in *JobList) DeepCopy() *JobList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(JobList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *JobList) 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 *JobSpec) DeepCopyInto(out *JobSpec) {
|
||||
*out = *in
|
||||
in.Template.DeepCopyInto(&out.Template)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobSpec.
|
||||
func (in *JobSpec) DeepCopy() *JobSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(JobSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *JobStatus) DeepCopyInto(out *JobStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobStatus.
|
||||
func (in *JobStatus) DeepCopy() *JobStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(JobStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Velero) DeepCopyInto(out *Velero) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Velero.
|
||||
func (in *Velero) DeepCopy() *Velero {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Velero)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Velero) 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 *VeleroList) DeepCopyInto(out *VeleroList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Velero, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroList.
|
||||
func (in *VeleroList) DeepCopy() *VeleroList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VeleroList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *VeleroList) 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 *VeleroSpec) DeepCopyInto(out *VeleroSpec) {
|
||||
*out = *in
|
||||
in.Template.DeepCopyInto(&out.Template)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroSpec.
|
||||
func (in *VeleroSpec) DeepCopy() *VeleroSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VeleroSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VeleroStatus) DeepCopyInto(out *VeleroStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroStatus.
|
||||
func (in *VeleroStatus) DeepCopy() *VeleroStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VeleroStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VeleroTemplate) DeepCopyInto(out *VeleroTemplate) {
|
||||
*out = *in
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.RestoreSpec.DeepCopyInto(&out.RestoreSpec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroTemplate.
|
||||
func (in *VeleroTemplate) DeepCopy() *VeleroTemplate {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VeleroTemplate)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
421
api/backups/v1alpha1/DESIGN.md
Normal file
421
api/backups/v1alpha1/DESIGN.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# Cozystack Backups – Core API & Contracts (Draft)
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Cozystack’s backup subsystem provides a generic, composable way to back up and restore managed applications:
|
||||
|
||||
* Every **application instance** can have one or more **backup plans**.
|
||||
* Backups are stored in configurable **storage locations**.
|
||||
* The mechanics of *how* a backup/restore is performed are delegated to **strategy drivers**, each implementing driver-specific **BackupStrategy** CRDs.
|
||||
|
||||
The core API:
|
||||
|
||||
* Orchestrates **when** backups happen and **where** they’re stored.
|
||||
* Tracks **what** backups exist and their status.
|
||||
* Defines contracts with drivers via shared resources (`BackupJob`, `Backup`, `RestoreJob`).
|
||||
|
||||
It does **not** implement the backup logic itself.
|
||||
|
||||
This document covers only the **core** API and its contracts with drivers, not driver implementations.
|
||||
|
||||
---
|
||||
|
||||
## 2. Goals and non-goals
|
||||
|
||||
### Goals
|
||||
|
||||
* Provide a **stable core API** for:
|
||||
|
||||
* Declaring **backup plans** per application.
|
||||
* Configuring **storage targets** (S3, in-cluster bucket, etc.).
|
||||
* Tracking **backup artifacts**.
|
||||
* Initiating and tracking **restores**.
|
||||
* Allow multiple **strategy drivers** to plug in, each supporting specific kinds of applications and strategies.
|
||||
* Let application/product authors implement backup for their kinds by:
|
||||
|
||||
* Creating **Plan** objects referencing a **driver-specific strategy**.
|
||||
* Not having to write a backup engine themselves.
|
||||
|
||||
### Non-goals
|
||||
|
||||
* Implement backup logic for any specific application or storage backend.
|
||||
* Define the internal structure of driver-specific strategy CRDs.
|
||||
* Handle tenant-facing UI/UX (that’s built on top of these APIs).
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
High-level components:
|
||||
|
||||
* **Core backups controller(s)** (Cozystack-owned):
|
||||
|
||||
* Group: `backups.cozystack.io`
|
||||
* Own:
|
||||
|
||||
* `Plan`
|
||||
* `BackupJob`
|
||||
* `Backup`
|
||||
* `RestoreJob`
|
||||
* Responsibilities:
|
||||
|
||||
* Schedule backups based on `Plan`.
|
||||
* Create `BackupJob` objects when due.
|
||||
* Provide stable contracts for drivers to:
|
||||
|
||||
* Perform backups and create `Backup`s.
|
||||
* Perform restores based on `Backup`s.
|
||||
|
||||
* **Strategy drivers** (pluggable, possibly third-party):
|
||||
|
||||
* Their own API groups, e.g. `jobdriver.backups.cozystack.io`.
|
||||
* Own **strategy CRDs** (e.g. `JobBackupStrategy`).
|
||||
* Implement controllers that:
|
||||
|
||||
* Watch `BackupJob` / `RestoreJob`.
|
||||
* Match runs whose `strategyRef` GVK they support.
|
||||
* Execute backup/restore logic.
|
||||
* Create and update `Backup` and run statuses.
|
||||
|
||||
Strategy drivers and core communicate entirely via Kubernetes objects; there are no webhook/HTTP calls between them.
|
||||
|
||||
* **Storage drivers** (pluggable, possibly third-party):
|
||||
|
||||
* **TBD**
|
||||
|
||||
---
|
||||
|
||||
## 4. Core API resources
|
||||
|
||||
### 4.1 Plan
|
||||
|
||||
**Group/Kind**
|
||||
`backups.cozystack.io/v1alpha1, Kind=Plan`
|
||||
|
||||
**Purpose**
|
||||
Describe **when**, **how**, and **where** to back up a specific managed application.
|
||||
|
||||
**Key fields (spec)**
|
||||
|
||||
```go
|
||||
type PlanSpec struct {
|
||||
// Application to back up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// Where backups should be stored.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// Driver-specific BackupStrategy to use.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
|
||||
// When backups should run.
|
||||
Schedule PlanSchedule `json:"schedule"`
|
||||
}
|
||||
```
|
||||
|
||||
`PlanSchedule` (initially) supports only cron:
|
||||
|
||||
```go
|
||||
type PlanScheduleType string
|
||||
|
||||
const (
|
||||
PlanScheduleTypeEmpty PlanScheduleType = ""
|
||||
PlanScheduleTypeCron PlanScheduleType = "cron"
|
||||
)
|
||||
```
|
||||
|
||||
```go
|
||||
type PlanSchedule struct {
|
||||
// Type is the schedule type. Currently only "cron" is supported.
|
||||
// Defaults to "cron".
|
||||
Type PlanScheduleType `json:"type,omitempty"`
|
||||
|
||||
// Cron expression (required for cron type).
|
||||
Cron string `json:"cron,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**Plan reconciliation contract**
|
||||
|
||||
Core Plan controller:
|
||||
|
||||
1. **Read schedule** from `spec.schedule` and compute the next fire time.
|
||||
2. When due:
|
||||
|
||||
* Create a `BackupJob` in the same namespace:
|
||||
|
||||
* `spec.planRef.name = plan.Name`
|
||||
* `spec.applicationRef = plan.spec.applicationRef`
|
||||
* `spec.storageRef = plan.spec.storageRef`
|
||||
* `spec.strategyRef = plan.spec.strategyRef`
|
||||
* `spec.triggeredBy = "Plan"`
|
||||
* Set `ownerReferences` so the `BackupJob` is owned by the `Plan`.
|
||||
|
||||
The Plan controller does **not**:
|
||||
|
||||
* Execute backups itself.
|
||||
* Modify driver resources or `Backup` objects.
|
||||
* Touch `BackupJob.spec` after creation.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Storage
|
||||
|
||||
**API Shape**
|
||||
|
||||
TBD
|
||||
|
||||
**Storage usage**
|
||||
|
||||
* `Plan` and `BackupJob` reference `Storage` via `TypedLocalObjectReference`.
|
||||
* Drivers read `Storage` to know how/where to store or read artifacts.
|
||||
* Core treats `Storage` spec as opaque; it does not directly talk to S3 or buckets.
|
||||
|
||||
---
|
||||
|
||||
### 4.3 BackupJob
|
||||
|
||||
**Group/Kind**
|
||||
`backups.cozystack.io/v1alpha1, Kind=BackupJob`
|
||||
|
||||
**Purpose**
|
||||
Represent a single **execution** of a backup operation, typically created when a `Plan` fires or when a user triggers an ad-hoc backup.
|
||||
|
||||
**Key fields (spec)**
|
||||
|
||||
```go
|
||||
type BackupJobSpec struct {
|
||||
// Plan that triggered this run, if any.
|
||||
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
|
||||
|
||||
// Application to back up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// Storage to use.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// Driver-specific BackupStrategy to use.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
|
||||
// Informational: what triggered this run ("Plan", "Manual", etc.).
|
||||
TriggeredBy string `json:"triggeredBy,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields (status)**
|
||||
|
||||
```go
|
||||
type BackupJobStatus struct {
|
||||
Phase BackupJobPhase `json:"phase,omitempty"`
|
||||
BackupRef *corev1.LocalObjectReference `json:"backupRef,omitempty"`
|
||||
StartedAt *metav1.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
`BackupJobPhase` is one of: `Pending`, `Running`, `Succeeded`, `Failed`.
|
||||
|
||||
**BackupJob contract with drivers**
|
||||
|
||||
* Core **creates** `BackupJob` and must treat `spec` as immutable afterwards.
|
||||
* Each driver controller:
|
||||
|
||||
* Watches `BackupJob`.
|
||||
* Reconciles runs where `spec.strategyRef.apiGroup/kind` matches its **strategy type(s)**.
|
||||
* Driver responsibilities:
|
||||
|
||||
1. On first reconcile:
|
||||
|
||||
* Set `status.startedAt` if unset.
|
||||
* Set `status.phase = Running`.
|
||||
2. Resolve inputs:
|
||||
|
||||
* Read `Strategy` (driver-owned CRD), `Storage`, `Application`, optionally `Plan`.
|
||||
3. Execute backup logic (implementation-specific).
|
||||
4. On success:
|
||||
|
||||
* Create a `Backup` resource (see below).
|
||||
* Set `status.backupRef` to the created `Backup`.
|
||||
* Set `status.completedAt`.
|
||||
* Set `status.phase = Succeeded`.
|
||||
5. On failure:
|
||||
|
||||
* Set `status.completedAt`.
|
||||
* Set `status.phase = Failed`.
|
||||
* Set `status.message` and conditions.
|
||||
|
||||
Drivers must **not** modify `BackupJob.spec` or delete `BackupJob` themselves.
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Backup
|
||||
|
||||
**Group/Kind**
|
||||
`backups.cozystack.io/v1alpha1, Kind=Backup`
|
||||
|
||||
**Purpose**
|
||||
Represent a single **backup artifact** for a given application, decoupled from a particular run. usable as a stable, listable “thing you can restore from”.
|
||||
|
||||
**Key fields (spec)**
|
||||
|
||||
```go
|
||||
type BackupSpec struct {
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
TakenAt metav1.Time `json:"takenAt"`
|
||||
DriverMetadata map[string]string `json:"driverMetadata,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields (status)**
|
||||
|
||||
```go
|
||||
type BackupStatus struct {
|
||||
Phase BackupPhase `json:"phase,omitempty"` // Pending, Ready, Failed, etc.
|
||||
Artifact *BackupArtifact `json:"artifact,omitempty"`
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
`BackupArtifact` describes the artifact (URI, size, checksum).
|
||||
|
||||
**Backup contract with drivers**
|
||||
|
||||
* On successful completion of a `BackupJob`, the **driver**:
|
||||
|
||||
* Creates a `Backup` in the same namespace (typically owned by the `BackupJob`).
|
||||
* Populates `spec` fields with:
|
||||
|
||||
* The application, storage, strategy references.
|
||||
* `takenAt`.
|
||||
* Optional `driverMetadata`.
|
||||
* Sets `status` with:
|
||||
|
||||
* `phase = Ready` (or equivalent when fully usable).
|
||||
* `artifact` describing the stored object.
|
||||
* Core:
|
||||
|
||||
* Treats `Backup` spec as mostly immutable and opaque.
|
||||
* Uses it to:
|
||||
|
||||
* List backups for a given application/plan.
|
||||
* Anchor `RestoreJob` operations.
|
||||
* Implement higher-level policies (retention) if needed.
|
||||
|
||||
---
|
||||
|
||||
### 4.5 RestoreJob
|
||||
|
||||
**Group/Kind**
|
||||
`backups.cozystack.io/v1alpha1, Kind=RestoreJob`
|
||||
|
||||
**Purpose**
|
||||
Represent a single **restore operation** from a `Backup`, either back into the same application or into a new target application.
|
||||
|
||||
**Key fields (spec)**
|
||||
|
||||
```go
|
||||
type RestoreJobSpec struct {
|
||||
// Backup to restore from.
|
||||
BackupRef corev1.LocalObjectReference `json:"backupRef"`
|
||||
|
||||
// Target application; if omitted, drivers SHOULD restore into
|
||||
// backup.spec.applicationRef.
|
||||
TargetApplicationRef *corev1.TypedLocalObjectReference `json:"targetApplicationRef,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields (status)**
|
||||
|
||||
```go
|
||||
type RestoreJobStatus struct {
|
||||
Phase RestoreJobPhase `json:"phase,omitempty"` // Pending, Running, Succeeded, Failed
|
||||
StartedAt *metav1.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**RestoreJob contract with drivers**
|
||||
|
||||
* RestoreJob is created either manually or by core.
|
||||
* Driver controller:
|
||||
|
||||
1. Watches `RestoreJob`.
|
||||
2. On reconcile:
|
||||
|
||||
* Fetches the referenced `Backup`.
|
||||
* Determines effective:
|
||||
|
||||
* **Strategy**: `backup.spec.strategyRef`.
|
||||
* **Storage**: `backup.spec.storageRef`.
|
||||
* **Target application**: `spec.targetApplicationRef` or `backup.spec.applicationRef`.
|
||||
* If effective strategy’s GVK is one of its supported strategy types → driver is responsible.
|
||||
3. Behaviour:
|
||||
|
||||
* On first reconcile, set `status.startedAt` and `phase = Running`.
|
||||
* Resolve `Backup`, `Storage`, `Strategy`, target application.
|
||||
* Execute restore logic (implementation-specific).
|
||||
* On success:
|
||||
|
||||
* Set `status.completedAt`.
|
||||
* Set `status.phase = Succeeded`.
|
||||
* On failure:
|
||||
|
||||
* Set `status.completedAt`.
|
||||
* Set `status.phase = Failed`.
|
||||
* Set `status.message` and conditions.
|
||||
|
||||
Drivers must not modify `RestoreJob.spec` or delete `RestoreJob`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Strategy drivers (high-level)
|
||||
|
||||
Strategy drivers are separate controllers that:
|
||||
|
||||
* Define their own **strategy CRDs** (e.g. `JobBackupStrategy`) in their own API groups:
|
||||
|
||||
* e.g. `jobdriver.backups.cozystack.io/v1alpha1, Kind=JobBackupStrategy`
|
||||
* Implement the **BackupJob contract**:
|
||||
|
||||
* Watch `BackupJob`.
|
||||
* Filter by `spec.strategyRef.apiGroup/kind`.
|
||||
* Execute backup logic.
|
||||
* Create/update `Backup`.
|
||||
* Implement the **RestoreJob contract**:
|
||||
|
||||
* Watch `RestoreJob`.
|
||||
* Resolve `Backup`, then effective `strategyRef`.
|
||||
* Filter by effective strategy GVK.
|
||||
* Execute restore logic.
|
||||
|
||||
The core backups API **does not** dictate:
|
||||
|
||||
* The fields and structure of driver strategy specs.
|
||||
* How drivers implement backup/restore internally (Jobs, snapshots, native operator CRDs, etc.).
|
||||
|
||||
Drivers are interchangeable as long as they respect:
|
||||
|
||||
* The `BackupJob` and `RestoreJob` contracts.
|
||||
* The shapes and semantics of `Backup` objects.
|
||||
|
||||
---
|
||||
|
||||
## 6. Summary
|
||||
|
||||
The Cozystack backups core API:
|
||||
|
||||
* Uses a single group, `backups.cozystack.io`, for all core CRDs.
|
||||
* Cleanly separates:
|
||||
|
||||
* **When & where** (Plan + Storage) – core-owned.
|
||||
* **What backup artifacts exist** (Backup) – driver-created but cluster-visible.
|
||||
* **Execution lifecycle** (BackupJob, RestoreJob) – shared contract boundary.
|
||||
* Allows multiple strategy drivers to implement backup/restore logic without entangling their implementation with the core API.
|
||||
|
||||
118
api/backups/v1alpha1/backup_types.go
Normal file
118
api/backups/v1alpha1/backup_types.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines backups.cozystack.io API types.
|
||||
//
|
||||
// Group: backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&Backup{},
|
||||
&BackupList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// BackupPhase represents the lifecycle phase of a Backup.
|
||||
type BackupPhase string
|
||||
|
||||
const (
|
||||
BackupPhaseEmpty BackupPhase = ""
|
||||
BackupPhasePending BackupPhase = "Pending"
|
||||
BackupPhaseReady BackupPhase = "Ready"
|
||||
BackupPhaseFailed BackupPhase = "Failed"
|
||||
)
|
||||
|
||||
// BackupArtifact describes the stored backup object (tarball, snapshot, etc.).
|
||||
type BackupArtifact struct {
|
||||
// URI is a driver-/storage-specific URI pointing to the backup artifact.
|
||||
// For example: s3://bucket/prefix/file.tar.gz
|
||||
URI string `json:"uri"`
|
||||
|
||||
// SizeBytes is the size of the artifact in bytes, if known.
|
||||
// +optional
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
|
||||
// Checksum is the checksum of the artifact, if computed.
|
||||
// For example: "sha256:<hex>".
|
||||
// +optional
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
// BackupSpec describes an immutable backup artifact produced by a BackupJob.
|
||||
type BackupSpec struct {
|
||||
// ApplicationRef refers to the application that was backed up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// PlanRef refers to the Plan that produced this backup, if any.
|
||||
// For manually triggered backups, this can be omitted.
|
||||
// +optional
|
||||
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
|
||||
|
||||
// StorageRef refers to the Storage object that describes where the backup
|
||||
// artifact is stored.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// StrategyRef refers to the driver-specific BackupStrategy that was used
|
||||
// to create this backup. This allows the driver to later perform restores.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
|
||||
// TakenAt is the time at which the backup was taken (as reported by the
|
||||
// driver). It may differ slightly from metadata.creationTimestamp.
|
||||
TakenAt metav1.Time `json:"takenAt"`
|
||||
|
||||
// DriverMetadata holds driver-specific, opaque metadata associated with
|
||||
// this backup (for example snapshot IDs, schema versions, etc.).
|
||||
// This data is not interpreted by the core backup controllers.
|
||||
// +optional
|
||||
DriverMetadata map[string]string `json:"driverMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// BackupStatus represents the observed state of a Backup.
|
||||
type BackupStatus struct {
|
||||
// Phase is a simple, high-level summary of the backup's state.
|
||||
// Typical values are: Pending, Ready, Failed.
|
||||
// +optional
|
||||
Phase BackupPhase `json:"phase,omitempty"`
|
||||
|
||||
// Artifact describes the stored backup object, if available.
|
||||
// +optional
|
||||
Artifact *BackupArtifact `json:"artifact,omitempty"`
|
||||
|
||||
// Conditions represents the latest available observations of a Backup's state.
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// The field indexing on applicationRef will be needed later to display per-app backup resources.
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
|
||||
|
||||
// Backup represents a single backup artifact for a given application.
|
||||
type Backup struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec BackupSpec `json:"spec,omitempty"`
|
||||
Status BackupStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// BackupList contains a list of Backups.
|
||||
type BackupList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Backup `json:"items"`
|
||||
}
|
||||
116
api/backups/v1alpha1/backupjob_types.go
Normal file
116
api/backups/v1alpha1/backupjob_types.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines backups.cozystack.io API types.
|
||||
//
|
||||
// Group: backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&BackupJob{},
|
||||
&BackupJobList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
OwningJobNameLabel = thisGroup + "/owned-by.BackupJobName"
|
||||
OwningJobNamespaceLabel = thisGroup + "/owned-by.BackupJobNamespace"
|
||||
)
|
||||
|
||||
// BackupJobPhase represents the lifecycle phase of a BackupJob.
|
||||
type BackupJobPhase string
|
||||
|
||||
const (
|
||||
BackupJobPhaseEmpty BackupJobPhase = ""
|
||||
BackupJobPhasePending BackupJobPhase = "Pending"
|
||||
BackupJobPhaseRunning BackupJobPhase = "Running"
|
||||
BackupJobPhaseSucceeded BackupJobPhase = "Succeeded"
|
||||
BackupJobPhaseFailed BackupJobPhase = "Failed"
|
||||
)
|
||||
|
||||
// BackupJobSpec describes the execution of a single backup operation.
|
||||
type BackupJobSpec struct {
|
||||
// PlanRef refers to the Plan that requested this backup run.
|
||||
// For ad-hoc/manual backups, this can be omitted.
|
||||
// +optional
|
||||
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
|
||||
|
||||
// ApplicationRef holds a reference to the managed application whose state
|
||||
// is being backed up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// StorageRef holds a reference to the Storage object that describes where
|
||||
// the backup will be stored.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// StrategyRef holds a reference to the driver-specific BackupStrategy object
|
||||
// that describes how the backup should be created.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
}
|
||||
|
||||
// BackupJobStatus represents the observed state of a BackupJob.
|
||||
type BackupJobStatus struct {
|
||||
// Phase is a high-level summary of the run's state.
|
||||
// Typical values: Pending, Running, Succeeded, Failed.
|
||||
// +optional
|
||||
Phase BackupJobPhase `json:"phase,omitempty"`
|
||||
|
||||
// BackupRef refers to the Backup object created by this run, if any.
|
||||
// +optional
|
||||
BackupRef *corev1.LocalObjectReference `json:"backupRef,omitempty"`
|
||||
|
||||
// StartedAt is the time at which the backup run started.
|
||||
// +optional
|
||||
StartedAt *metav1.Time `json:"startedAt,omitempty"`
|
||||
|
||||
// CompletedAt is the time at which the backup run completed (successfully
|
||||
// or otherwise).
|
||||
// +optional
|
||||
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
|
||||
|
||||
// Message is a human-readable message indicating details about why the
|
||||
// backup run is in its current phase, if any.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// Conditions represents the latest available observations of a BackupJob's state.
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// The field indexing on applicationRef will be needed later to display per-app backup resources.
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",priority=0
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
|
||||
|
||||
// BackupJob represents a single execution of a backup.
|
||||
// It is typically created by a Plan controller when a schedule fires.
|
||||
type BackupJob struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec BackupJobSpec `json:"spec,omitempty"`
|
||||
Status BackupJobStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// BackupJobList contains a list of BackupJobs.
|
||||
type BackupJobList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []BackupJob `json:"items"`
|
||||
}
|
||||
42
api/backups/v1alpha1/groupversion_info.go
Normal file
42
api/backups/v1alpha1/groupversion_info.go
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package v1alpha1 contains API Schema definitions for the v1alpha1 API group.
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=backups.cozystack.io
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
thisGroup = "backups.cozystack.io"
|
||||
thisVersion = "v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
GroupVersion = schema.GroupVersion{Group: thisGroup, Version: thisVersion}
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addGroupVersion)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func addGroupVersion(scheme *runtime.Scheme) error {
|
||||
metav1.AddToGroupVersion(scheme, GroupVersion)
|
||||
return nil
|
||||
}
|
||||
98
api/backups/v1alpha1/plan_types.go
Normal file
98
api/backups/v1alpha1/plan_types.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines backups.cozystack.io API types.
|
||||
//
|
||||
// Group: backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&Plan{},
|
||||
&PlanList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type PlanScheduleType string
|
||||
|
||||
const (
|
||||
PlanScheduleTypeEmpty PlanScheduleType = ""
|
||||
PlanScheduleTypeCron PlanScheduleType = "cron"
|
||||
)
|
||||
|
||||
// Condtions
|
||||
const (
|
||||
PlanConditionError = "Error"
|
||||
)
|
||||
|
||||
// The field indexing on applicationRef will be needed later to display per-app backup resources.
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
|
||||
|
||||
// Plan describes the schedule, method and storage location for the
|
||||
// backup of a given target application.
|
||||
type Plan struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec PlanSpec `json:"spec,omitempty"`
|
||||
Status PlanStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PlanList contains a list of backup Plans.
|
||||
type PlanList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Plan `json:"items"`
|
||||
}
|
||||
|
||||
// PlanSpec references the storage, the strategy, the application to be
|
||||
// backed up and specifies the timetable on which the backups will run.
|
||||
type PlanSpec struct {
|
||||
// ApplicationRef holds a reference to the managed application,
|
||||
// whose state and configuration must be backed up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// StorageRef holds a reference to the Storage object that
|
||||
// describes the location where the backup will be stored.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// StrategyRef holds a reference to the Strategy object that
|
||||
// describes, how a backup copy is to be created.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
|
||||
// Schedule specifies when backup copies are created.
|
||||
Schedule PlanSchedule `json:"schedule"`
|
||||
}
|
||||
|
||||
// PlanSchedule specifies when backup copies are created.
|
||||
type PlanSchedule struct {
|
||||
// Type is the type of schedule specification. Supported values are
|
||||
// [`cron`]. If omitted, defaults to `cron`.
|
||||
// +optional
|
||||
Type PlanScheduleType `json:"type,omitempty"`
|
||||
|
||||
// Cron contains the cron spec for scheduling backups. Must be
|
||||
// specified if the schedule type is `cron`. Since only `cron` is
|
||||
// supported, omitting this field is not allowed.
|
||||
// +optional
|
||||
Cron string `json:"cron,omitempty"`
|
||||
}
|
||||
|
||||
type PlanStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
93
api/backups/v1alpha1/restorejob_types.go
Normal file
93
api/backups/v1alpha1/restorejob_types.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines backups.cozystack.io API types.
|
||||
//
|
||||
// Group: backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&RestoreJob{},
|
||||
&RestoreJobList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RestoreJobPhase represents the lifecycle phase of a RestoreJob.
|
||||
type RestoreJobPhase string
|
||||
|
||||
const (
|
||||
RestoreJobPhaseEmpty RestoreJobPhase = ""
|
||||
RestoreJobPhasePending RestoreJobPhase = "Pending"
|
||||
RestoreJobPhaseRunning RestoreJobPhase = "Running"
|
||||
RestoreJobPhaseSucceeded RestoreJobPhase = "Succeeded"
|
||||
RestoreJobPhaseFailed RestoreJobPhase = "Failed"
|
||||
)
|
||||
|
||||
// RestoreJobSpec describes the execution of a single restore operation.
|
||||
type RestoreJobSpec struct {
|
||||
// BackupRef refers to the Backup that should be restored.
|
||||
BackupRef corev1.LocalObjectReference `json:"backupRef"`
|
||||
|
||||
// TargetApplicationRef refers to the application into which the backup
|
||||
// should be restored. If omitted, the driver SHOULD restore into the same
|
||||
// application as referenced by backup.spec.applicationRef.
|
||||
// +optional
|
||||
TargetApplicationRef *corev1.TypedLocalObjectReference `json:"targetApplicationRef,omitempty"`
|
||||
}
|
||||
|
||||
// RestoreJobStatus represents the observed state of a RestoreJob.
|
||||
type RestoreJobStatus struct {
|
||||
// Phase is a high-level summary of the run's state.
|
||||
// Typical values: Pending, Running, Succeeded, Failed.
|
||||
// +optional
|
||||
Phase RestoreJobPhase `json:"phase,omitempty"`
|
||||
|
||||
// StartedAt is the time at which the restore run started.
|
||||
// +optional
|
||||
StartedAt *metav1.Time `json:"startedAt,omitempty"`
|
||||
|
||||
// CompletedAt is the time at which the restore run completed (successfully
|
||||
// or otherwise).
|
||||
// +optional
|
||||
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
|
||||
|
||||
// Message is a human-readable message indicating details about why the
|
||||
// restore run is in its current phase, if any.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// Conditions represents the latest available observations of a RestoreJob's state.
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",priority=0
|
||||
|
||||
// RestoreJob represents a single execution of a restore from a Backup.
|
||||
type RestoreJob struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec RestoreJobSpec `json:"spec,omitempty"`
|
||||
Status RestoreJobStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// RestoreJobList contains a list of RestoreJobs.
|
||||
type RestoreJobList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []RestoreJob `json:"items"`
|
||||
}
|
||||
501
api/backups/v1alpha1/zz_generated.deepcopy.go
Normal file
501
api/backups/v1alpha1/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,501 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Backup) DeepCopyInto(out *Backup) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backup.
|
||||
func (in *Backup) DeepCopy() *Backup {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Backup)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Backup) 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 *BackupArtifact) DeepCopyInto(out *BackupArtifact) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupArtifact.
|
||||
func (in *BackupArtifact) DeepCopy() *BackupArtifact {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupArtifact)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupJob) DeepCopyInto(out *BackupJob) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJob.
|
||||
func (in *BackupJob) DeepCopy() *BackupJob {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupJob)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BackupJob) 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 *BackupJobList) DeepCopyInto(out *BackupJobList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]BackupJob, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobList.
|
||||
func (in *BackupJobList) DeepCopy() *BackupJobList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupJobList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BackupJobList) 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 *BackupJobSpec) DeepCopyInto(out *BackupJobSpec) {
|
||||
*out = *in
|
||||
if in.PlanRef != nil {
|
||||
in, out := &in.PlanRef, &out.PlanRef
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
|
||||
in.StorageRef.DeepCopyInto(&out.StorageRef)
|
||||
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobSpec.
|
||||
func (in *BackupJobSpec) DeepCopy() *BackupJobSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupJobSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupJobStatus) DeepCopyInto(out *BackupJobStatus) {
|
||||
*out = *in
|
||||
if in.BackupRef != nil {
|
||||
in, out := &in.BackupRef, &out.BackupRef
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.StartedAt != nil {
|
||||
in, out := &in.StartedAt, &out.StartedAt
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.CompletedAt != nil {
|
||||
in, out := &in.CompletedAt, &out.CompletedAt
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobStatus.
|
||||
func (in *BackupJobStatus) DeepCopy() *BackupJobStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupJobStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupList) DeepCopyInto(out *BackupList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Backup, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupList.
|
||||
func (in *BackupList) DeepCopy() *BackupList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BackupList) 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 *BackupSpec) DeepCopyInto(out *BackupSpec) {
|
||||
*out = *in
|
||||
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
|
||||
if in.PlanRef != nil {
|
||||
in, out := &in.PlanRef, &out.PlanRef
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
in.StorageRef.DeepCopyInto(&out.StorageRef)
|
||||
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
|
||||
in.TakenAt.DeepCopyInto(&out.TakenAt)
|
||||
if in.DriverMetadata != nil {
|
||||
in, out := &in.DriverMetadata, &out.DriverMetadata
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSpec.
|
||||
func (in *BackupSpec) DeepCopy() *BackupSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupStatus) DeepCopyInto(out *BackupStatus) {
|
||||
*out = *in
|
||||
if in.Artifact != nil {
|
||||
in, out := &in.Artifact, &out.Artifact
|
||||
*out = new(BackupArtifact)
|
||||
**out = **in
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupStatus.
|
||||
func (in *BackupStatus) DeepCopy() *BackupStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Plan) DeepCopyInto(out *Plan) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Plan.
|
||||
func (in *Plan) DeepCopy() *Plan {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Plan)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Plan) 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 *PlanList) DeepCopyInto(out *PlanList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Plan, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanList.
|
||||
func (in *PlanList) DeepCopy() *PlanList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlanList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PlanList) 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 *PlanSchedule) DeepCopyInto(out *PlanSchedule) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanSchedule.
|
||||
func (in *PlanSchedule) DeepCopy() *PlanSchedule {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlanSchedule)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PlanSpec) DeepCopyInto(out *PlanSpec) {
|
||||
*out = *in
|
||||
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
|
||||
in.StorageRef.DeepCopyInto(&out.StorageRef)
|
||||
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
|
||||
out.Schedule = in.Schedule
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanSpec.
|
||||
func (in *PlanSpec) DeepCopy() *PlanSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlanSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PlanStatus) DeepCopyInto(out *PlanStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanStatus.
|
||||
func (in *PlanStatus) DeepCopy() *PlanStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlanStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RestoreJob) DeepCopyInto(out *RestoreJob) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJob.
|
||||
func (in *RestoreJob) DeepCopy() *RestoreJob {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RestoreJob)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *RestoreJob) 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 *RestoreJobList) DeepCopyInto(out *RestoreJobList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]RestoreJob, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobList.
|
||||
func (in *RestoreJobList) DeepCopy() *RestoreJobList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RestoreJobList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *RestoreJobList) 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 *RestoreJobSpec) DeepCopyInto(out *RestoreJobSpec) {
|
||||
*out = *in
|
||||
out.BackupRef = in.BackupRef
|
||||
if in.TargetApplicationRef != nil {
|
||||
in, out := &in.TargetApplicationRef, &out.TargetApplicationRef
|
||||
*out = new(v1.TypedLocalObjectReference)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobSpec.
|
||||
func (in *RestoreJobSpec) DeepCopy() *RestoreJobSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RestoreJobSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RestoreJobStatus) DeepCopyInto(out *RestoreJobStatus) {
|
||||
*out = *in
|
||||
if in.StartedAt != nil {
|
||||
in, out := &in.StartedAt, &out.StartedAt
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.CompletedAt != nil {
|
||||
in, out := &in.CompletedAt, &out.CompletedAt
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobStatus.
|
||||
func (in *RestoreJobStatus) DeepCopy() *RestoreJobStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RestoreJobStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
174
cmd/backup-controller/main.go
Normal file
174
cmd/backup-controller/main.go
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/backupcontroller"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(backupsv1alpha1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
opts := zap.Options{
|
||||
Development: false,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhook.Options{
|
||||
TLSOpts: tlsOpts,
|
||||
})
|
||||
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
|
||||
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
}
|
||||
|
||||
// Configure rate limiting for the Kubernetes client
|
||||
config := ctrl.GetConfigOrDie()
|
||||
config.QPS = 50.0 // Increased from default 5.0
|
||||
config.Burst = 100 // Increased from default 10
|
||||
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "core.backups.cozystack.io",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
// LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&backupcontroller.PlanReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Plan")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
188
cmd/backupstrategy-controller/main.go
Normal file
188
cmd/backupstrategy-controller/main.go
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/backupcontroller"
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(backupsv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(strategyv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(velerov1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
opts := zap.Options{
|
||||
Development: false,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhook.Options{
|
||||
TLSOpts: tlsOpts,
|
||||
})
|
||||
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
|
||||
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
}
|
||||
|
||||
// Configure rate limiting for the Kubernetes client
|
||||
config := ctrl.GetConfigOrDie()
|
||||
config.QPS = 50.0 // Increased from default 5.0
|
||||
config.Burst = 100 // Increased from default 10
|
||||
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "strategy.backups.cozystack.io",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
// LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&backupcontroller.BackupJobReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Recorder: mgr.GetEventRecorderFor("backup-controller"),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "BackupJob")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&backupcontroller.RestoreJobReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Recorder: mgr.GetEventRecorderFor("restore-controller"),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "RestoreJob")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
20
examples/desired-backup.yaml
Normal file
20
examples/desired-backup.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: backups.cozystack.io/v1alpha1
|
||||
kind: BackupJob
|
||||
metadata:
|
||||
name: desired-backup
|
||||
namespace: tenant-root
|
||||
labels:
|
||||
backups.cozystack.io/triggered-by: manual
|
||||
spec:
|
||||
applicationRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: VirtualMachine
|
||||
name: vm1
|
||||
storageRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: Bucket
|
||||
name: test-bucket
|
||||
strategyRef:
|
||||
apiGroup: strategy.backups.cozystack.io
|
||||
kind: Velero
|
||||
name: velero-strategy-default
|
||||
145
go.mod
145
go.mod
@@ -2,33 +2,41 @@
|
||||
|
||||
module github.com/cozystack/cozystack
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.5
|
||||
|
||||
require (
|
||||
github.com/fluxcd/helm-controller/api v1.1.0
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/gofuzz v1.2.0
|
||||
github.com/onsi/ginkgo/v2 v2.19.0
|
||||
github.com/onsi/gomega v1.33.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/onsi/ginkgo/v2 v2.22.0
|
||||
github.com/onsi/gomega v1.36.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vmware-tanzu/velero v1.17.1
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.31.2
|
||||
k8s.io/apiextensions-apiserver v0.31.2
|
||||
k8s.io/apimachinery v0.31.2
|
||||
k8s.io/apiserver v0.31.2
|
||||
k8s.io/client-go v0.31.2
|
||||
k8s.io/component-base v0.31.2
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/apiserver v0.34.1
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/component-base v0.34.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||
sigs.k8s.io/controller-runtime v0.19.0
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
@@ -36,86 +44,89 @@ require (
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.6.1 // indirect
|
||||
github.com/fluxcd/pkg/apis/meta v1.6.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/cel-go v0.21.0 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/moby/spdystream v0.4.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.16 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.16 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.4 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/term v0.33.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/kms v0.31.2 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
k8s.io/kms v0.34.1 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
// See: issues.k8s.io/135537
|
||||
//replace k8s.io/apimachinery => github.com/cozystack/apimachinery v0.0.0-20251201201312-18e522a87614
|
||||
|
||||
324
go.sum
324
go.sum
@@ -1,11 +1,11 @@
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
@@ -18,7 +18,7 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -26,12 +26,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
|
||||
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fluxcd/helm-controller/api v1.1.0 h1:NS5Wm3U6Kv4w7Cw2sDOV++vf2ecGfFV00x1+2Y3QcOY=
|
||||
@@ -40,13 +40,13 @@ github.com/fluxcd/pkg/apis/kustomize v1.6.1 h1:22FJc69Mq4i8aCxnKPlddHhSMyI4UPkQk
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.6.1/go.mod h1:5dvQ4IZwz0hMGmuj8tTWGtarsuxW0rWsxJOwC6i+0V8=
|
||||
github.com/fluxcd/pkg/apis/meta v1.6.1 h1:maLhcRJ3P/70ArLCY/LF/YovkxXbX+6sTWZwZQBeNq0=
|
||||
github.com/fluxcd/pkg/apis/meta v1.6.1/go.mod h1:YndB/gxgGZmKfqpAfFxyCDNFJFP0ikpeJzs66jwq280=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
@@ -64,50 +64,48 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
|
||||
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -115,111 +113,126 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
|
||||
github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
|
||||
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
|
||||
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
|
||||
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
|
||||
github.com/vmware-tanzu/velero v1.17.1 h1:ldKeiTuUwkThOw7zrUucNA1NwnLG66zl13YetWAoE0I=
|
||||
github.com/vmware-tanzu/velero v1.17.1/go.mod h1:3KTxuUN6Un38JzmYAX+8U6j2k6EexGoNNxa8jrJML8U=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=
|
||||
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||
go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0=
|
||||
go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E=
|
||||
go.etcd.io/etcd/client/v2 v2.305.13 h1:RWfV1SX5jTU0lbCvpVQe3iPQeAHETWdOTb6pxhd77C8=
|
||||
go.etcd.io/etcd/client/v2 v2.305.13/go.mod h1:iQnL7fepbiomdXMb3om1rHq96htNNGv2sJkEcZGDRRg=
|
||||
go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE=
|
||||
go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13 h1:st9bDWNsKkBNpP4PR1MvM/9NqUPfvYZx/YXegsYEH8M=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13/go.mod h1:N+4PLrp7agI/Viy+dUYpX7iRtSPvKq+w8Y14d1vX+m0=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw=
|
||||
go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok=
|
||||
go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
|
||||
go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
|
||||
go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
|
||||
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA=
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE=
|
||||
go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU=
|
||||
go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg=
|
||||
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
|
||||
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -228,50 +241,48 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -281,39 +292,44 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
|
||||
k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
|
||||
k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0=
|
||||
k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM=
|
||||
k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
|
||||
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4=
|
||||
k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE=
|
||||
k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
|
||||
k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
|
||||
k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA=
|
||||
k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ=
|
||||
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
|
||||
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
|
||||
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
|
||||
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
|
||||
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
|
||||
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kms v0.31.2 h1:pyx7l2qVOkClzFMIWMVF/FxsSkgd+OIGH7DecpbscJI=
|
||||
k8s.io/kms v0.31.2/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94=
|
||||
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 h1:GKE9U8BH16uynoxQii0auTjmmmuZ3O0LFMN6S0lPPhI=
|
||||
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
|
||||
sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
k8s.io/kms v0.34.1 h1:iCFOvewDPzWM9fMTfyIPO+4MeuZ0tcZbugxLNSHFG4w=
|
||||
k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
226
hack/e2e-apps/backup.bats.disabled
Executable file
226
hack/e2e-apps/backup.bats.disabled
Executable file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
# Test variables - stored for teardown
|
||||
TEST_NAMESPACE='tenant-test'
|
||||
TEST_BUCKET_NAME='test-backup-bucket'
|
||||
TEST_VM_NAME='test-backup-vm'
|
||||
TEST_BACKUPJOB_NAME='test-backup-job'
|
||||
|
||||
teardown() {
|
||||
# Clean up resources (runs even if test fails)
|
||||
namespace="${TEST_NAMESPACE}"
|
||||
bucket_name="${TEST_BUCKET_NAME}"
|
||||
vm_name="${TEST_VM_NAME}"
|
||||
backupjob_name="${TEST_BACKUPJOB_NAME}"
|
||||
|
||||
# Clean up port-forward if still running
|
||||
pkill -f "kubectl.*port-forward.*seaweedfs-s3" 2>/dev/null || true
|
||||
|
||||
# Clean up Velero resources in cozy-velero namespace
|
||||
# Find Velero backup by pattern matching namespace-backupjob
|
||||
for backup in $(kubectl -n cozy-velero get backups.velero.io -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true); do
|
||||
if echo "$backup" | grep -q "^${namespace}-${backupjob_name}-"; then
|
||||
kubectl -n cozy-velero delete backups.velero.io ${backup} --wait=false 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Clean up BackupStorageLocation and VolumeSnapshotLocation (named: namespace-backupjob)
|
||||
BSL_NAME="${namespace}-${backupjob_name}"
|
||||
kubectl -n cozy-velero delete backupstoragelocations.velero.io ${BSL_NAME} --wait=false 2>/dev/null || true
|
||||
kubectl -n cozy-velero delete volumesnapshotlocations.velero.io ${BSL_NAME} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up Velero credentials secret
|
||||
SECRET_NAME="backup-${namespace}-${backupjob_name}-s3-credentials"
|
||||
kubectl -n cozy-velero delete secret ${SECRET_NAME} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up BackupJob
|
||||
kubectl -n ${namespace} delete backupjob ${backupjob_name} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up Virtual Machine
|
||||
kubectl -n ${namespace} delete virtualmachines.apps.cozystack.io ${vm_name} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up Bucket
|
||||
kubectl -n ${namespace} delete bucket.apps.cozystack.io ${bucket_name} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up temporary files
|
||||
rm -f /tmp/bucket-backup-credentials.json
|
||||
}
|
||||
|
||||
print_log() {
|
||||
echo "# $1" >&3
|
||||
}
|
||||
|
||||
@test "Create Backup for Virtual Machine" {
|
||||
# Test variables
|
||||
bucket_name="${TEST_BUCKET_NAME}"
|
||||
vm_name="${TEST_VM_NAME}"
|
||||
backupjob_name="${TEST_BACKUPJOB_NAME}"
|
||||
namespace="${TEST_NAMESPACE}"
|
||||
|
||||
print_log "Step 0:Ensure BackupJob and Velero strategy CRDs are installed"
|
||||
kubectl apply -f packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml
|
||||
kubectl apply -f packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml
|
||||
# Wait for CRDs to be ready
|
||||
kubectl wait --for condition=established --timeout=30s crd backupjobs.backups.cozystack.io
|
||||
kubectl wait --for condition=established --timeout=30s crd veleroes.strategy.backups.cozystack.io
|
||||
|
||||
# Ensure velero-strategy-default resource exists
|
||||
kubectl apply -f packages/system/backup-controller/templates/strategy.yaml
|
||||
|
||||
print_log "Step 1: Create the bucket resource"
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: Bucket
|
||||
metadata:
|
||||
name: ${bucket_name}
|
||||
namespace: ${namespace}
|
||||
spec: {}
|
||||
EOF
|
||||
|
||||
print_log "Wait for the bucket to be ready"
|
||||
kubectl -n ${namespace} wait hr bucket-${bucket_name} --timeout=100s --for=condition=ready
|
||||
kubectl -n ${namespace} wait bucketclaims.objectstorage.k8s.io bucket-${bucket_name} --timeout=300s --for=jsonpath='{.status.bucketReady}'=true
|
||||
kubectl -n ${namespace} wait bucketaccesses.objectstorage.k8s.io bucket-${bucket_name} --timeout=300s --for=jsonpath='{.status.accessGranted}'=true
|
||||
|
||||
# Get bucket credentials for later S3 verification
|
||||
kubectl -n ${namespace} get secret bucket-${bucket_name} -ojsonpath='{.data.BucketInfo}' | base64 -d > /tmp/bucket-backup-credentials.json
|
||||
ACCESS_KEY=$(jq -r '.spec.secretS3.accessKeyID' /tmp/bucket-backup-credentials.json)
|
||||
SECRET_KEY=$(jq -r '.spec.secretS3.accessSecretKey' /tmp/bucket-backup-credentials.json)
|
||||
BUCKET_NAME=$(jq -r '.spec.bucketName' /tmp/bucket-backup-credentials.json)
|
||||
|
||||
print_log "Step 2: Create the Virtual Machine"
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: VirtualMachine
|
||||
metadata:
|
||||
name: ${vm_name}
|
||||
namespace: ${namespace}
|
||||
spec:
|
||||
external: false
|
||||
externalMethod: PortList
|
||||
externalPorts:
|
||||
- 22
|
||||
instanceType: "u1.medium"
|
||||
instanceProfile: ubuntu
|
||||
systemDisk:
|
||||
image: ubuntu
|
||||
storage: 5Gi
|
||||
storageClass: replicated
|
||||
gpus: []
|
||||
resources: {}
|
||||
sshKeys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF
|
||||
test@test
|
||||
cloudInit: |
|
||||
#cloud-config
|
||||
users:
|
||||
- name: test
|
||||
shell: /bin/bash
|
||||
sudo: ['ALL=(ALL) NOPASSWD: ALL']
|
||||
groups: sudo
|
||||
ssh_authorized_keys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF test@test
|
||||
cloudInitSeed: ""
|
||||
EOF
|
||||
|
||||
print_log "Wait for VM to be ready"
|
||||
sleep 5
|
||||
kubectl -n ${namespace} wait hr virtual-machine-${vm_name} --timeout=10s --for=condition=ready
|
||||
kubectl -n ${namespace} wait dv virtual-machine-${vm_name} --timeout=150s --for=condition=ready
|
||||
kubectl -n ${namespace} wait pvc virtual-machine-${vm_name} --timeout=100s --for=jsonpath='{.status.phase}'=Bound
|
||||
kubectl -n ${namespace} wait vm virtual-machine-${vm_name} --timeout=100s --for=condition=ready
|
||||
|
||||
print_log "Step 3: Create BackupJob"
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: backups.cozystack.io/v1alpha1
|
||||
kind: BackupJob
|
||||
metadata:
|
||||
name: ${backupjob_name}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
backups.cozystack.io/triggered-by: e2e-test
|
||||
spec:
|
||||
applicationRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: VirtualMachine
|
||||
name: ${vm_name}
|
||||
storageRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: Bucket
|
||||
name: ${bucket_name}
|
||||
strategyRef:
|
||||
apiGroup: strategy.backups.cozystack.io
|
||||
kind: Velero
|
||||
name: velero-strategy-default
|
||||
EOF
|
||||
|
||||
print_log "Wait for BackupJob to start"
|
||||
kubectl -n ${namespace} wait backupjob ${backupjob_name} --timeout=60s --for=jsonpath='{.status.phase}'=Running
|
||||
|
||||
print_log "Wait for BackupJob to complete"
|
||||
kubectl -n ${namespace} wait backupjob ${backupjob_name} --timeout=300s --for=jsonpath='{.status.phase}'=Succeeded
|
||||
|
||||
print_log "Verify BackupJob status"
|
||||
PHASE=$(kubectl -n ${namespace} get backupjob ${backupjob_name} -o jsonpath='{.status.phase}')
|
||||
[ "$PHASE" = "Succeeded" ]
|
||||
|
||||
# Verify BackupJob has a backupRef
|
||||
BACKUP_REF=$(kubectl -n ${namespace} get backupjob ${backupjob_name} -o jsonpath='{.status.backupRef.name}')
|
||||
[ -n "$BACKUP_REF" ]
|
||||
|
||||
# Find the Velero backup by searching for backups matching the namespace-backupjob pattern
|
||||
# Format: namespace-backupjob-timestamp
|
||||
VELERO_BACKUP_NAME=""
|
||||
VELERO_BACKUP_PHASE=""
|
||||
|
||||
print_log "Wait a bit for the backup to be created and appear in the API"
|
||||
sleep 30
|
||||
|
||||
# Find backup by pattern matching namespace-backupjob
|
||||
for backup in $(kubectl -n cozy-velero get backups.velero.io -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
|
||||
if echo "$backup" | grep -q "^${namespace}-${backupjob_name}-"; then
|
||||
VELERO_BACKUP_NAME=$backup
|
||||
VELERO_BACKUP_PHASE=$(kubectl -n cozy-velero get backups.velero.io $backup -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
print_log "Verify Velero Backup was found"
|
||||
[ -n "$VELERO_BACKUP_NAME" ]
|
||||
|
||||
echo '# Wait for Velero Backup to complete' >&3
|
||||
until kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' | grep -q 'Completed\|Failed'; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
print_log "Verify Velero Backup is Completed"
|
||||
timeout 90 sh -ec "until [ \"\$(kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' 2>/dev/null)\" = \"Completed\" ]; do sleep 30; done"
|
||||
|
||||
# Final verification
|
||||
VELERO_BACKUP_PHASE=$(kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
|
||||
[ "$VELERO_BACKUP_PHASE" = "Completed" ]
|
||||
|
||||
print_log "Step 4: Verify S3 has backup data"
|
||||
# Start port-forwarding to S3 service (with timeout to keep it alive)
|
||||
bash -c 'timeout 100s kubectl port-forward service/seaweedfs-s3 -n tenant-root 8333:8333 > /dev/null 2>&1 &'
|
||||
|
||||
# Wait for port-forward to be ready
|
||||
timeout 30 sh -ec "until nc -z localhost 8333; do sleep 1; done"
|
||||
|
||||
# Wait a bit for backup data to be written to S3
|
||||
sleep 30
|
||||
|
||||
# Set up MinIO client with insecure flag (use environment variable for all commands)
|
||||
export MC_INSECURE=1
|
||||
mc alias set local https://localhost:8333 $ACCESS_KEY $SECRET_KEY
|
||||
|
||||
# Verify backup directory exists in S3
|
||||
BACKUP_PATH="${BUCKET_NAME}/backups/${VELERO_BACKUP_NAME}"
|
||||
mc ls local/${BACKUP_PATH}/ 2>/dev/null
|
||||
[ $? -eq 0 ]
|
||||
|
||||
# Verify backup files exist (at least metadata files)
|
||||
BACKUP_FILES=$(mc ls local/${BACKUP_PATH}/ 2>/dev/null | wc -l || echo "0")
|
||||
[ "$BACKUP_FILES" -gt "0" ]
|
||||
}
|
||||
|
||||
@@ -21,14 +21,33 @@
|
||||
}
|
||||
|
||||
@test "Test kinds" {
|
||||
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.kind')
|
||||
if [ "$val" != "TenantList" ]; then
|
||||
echo "Expected kind to be TenantList, got $val"
|
||||
exit 1
|
||||
fi
|
||||
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.items[0].kind')
|
||||
if [ "$val" != "Tenant" ]; then
|
||||
echo "Expected kind to be Tenant, got $val"
|
||||
exit 1
|
||||
fi
|
||||
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.kind')
|
||||
if [ "$val" != "IngressList" ]; then
|
||||
echo "Expected kind to be IngressList, got $val"
|
||||
exit 1
|
||||
fi
|
||||
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.items[0].kind')
|
||||
if [ "$val" != "Ingress" ]; then
|
||||
echo "Expected kind to be Ingress, got $val"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "Create and delete namespace" {
|
||||
kubectl create ns cozy-test-create-and-delete-namespace --dry-run=client -o yaml | kubectl apply -f -
|
||||
if ! kubectl delete ns cozy-test-create-and-delete-namespace; then
|
||||
echo "Failed to delete namespace"
|
||||
kubectl describe ns cozy-test-create-and-delete-namespace
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -23,6 +23,13 @@ CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-
|
||||
API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-rules"}"
|
||||
UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}"
|
||||
CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4"
|
||||
TMPDIR=$(mktemp -d)
|
||||
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/crds
|
||||
COZY_RD_CRDDIR=packages/system/cozystack-resource-definition-crd/definition
|
||||
BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions
|
||||
BACKUPSTRATEGY_CRDDIR=packages/system/backupstrategy-controller/definitions
|
||||
|
||||
trap 'rm -rf ${TMPDIR}' EXIT
|
||||
|
||||
source "${CODEGEN_PKG}/kube_codegen.sh"
|
||||
|
||||
@@ -53,6 +60,12 @@ kube::codegen::gen_openapi \
|
||||
"${SCRIPT_ROOT}/pkg/apis"
|
||||
|
||||
$CONTROLLER_GEN object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
|
||||
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=packages/system/cozystack-controller/crds
|
||||
mv packages/system/cozystack-controller/crds/cozystack.io_cozystackresourcedefinitions.yaml \
|
||||
packages/system/cozystack-resource-definition-crd/definition/cozystack.io_cozystackresourcedefinitions.yaml
|
||||
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=${TMPDIR}
|
||||
|
||||
mv ${TMPDIR}/cozystack.io_cozystackresourcedefinitions.yaml \
|
||||
${COZY_RD_CRDDIR}/cozystack.io_cozystackresourcedefinitions.yaml
|
||||
|
||||
mv ${TMPDIR}/backups.cozystack.io*.yaml ${BACKUPS_CORE_CRDDIR}/
|
||||
mv ${TMPDIR}/strategy.backups.cozystack.io*.yaml ${BACKUPSTRATEGY_CRDDIR}/
|
||||
|
||||
mv ${TMPDIR}/*.yaml ${COZY_CONTROLLER_CRDDIR}/
|
||||
|
||||
111
internal/backupcontroller/backupjob_controller.go
Normal file
111
internal/backupcontroller/backupjob_controller.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
)
|
||||
|
||||
// BackupVeleroStrategyReconciler reconciles BackupJob with a strategy referencing
|
||||
// Velero.strategy.backups.cozystack.io objects.
|
||||
type BackupJobReconciler struct {
|
||||
client.Client
|
||||
dynamic.Interface
|
||||
meta.RESTMapper
|
||||
Scheme *runtime.Scheme
|
||||
Recorder record.EventRecorder
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("reconciling BackupJob", "namespace", req.Namespace, "name", req.Name)
|
||||
|
||||
j := &backupsv1alpha1.BackupJob{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, j)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
logger.V(1).Info("BackupJob not found, skipping")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Error(err, "failed to get BackupJob")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if j.Spec.StrategyRef.APIGroup == nil {
|
||||
return r.markBackupJobFailed(ctx, j, "StrategyRef.APIGroup is nil")
|
||||
}
|
||||
|
||||
if *j.Spec.StrategyRef.APIGroup != strategyv1alpha1.GroupVersion.Group {
|
||||
// skip if the strategy group foreign to this controller
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
logger.Info("processing BackupJob", "backupjob", j.Name, "strategyKind", j.Spec.StrategyRef.Kind)
|
||||
switch j.Spec.StrategyRef.Kind {
|
||||
case strategyv1alpha1.JobStrategyKind:
|
||||
return r.reconcileJob(ctx, j)
|
||||
case strategyv1alpha1.VeleroStrategyKind:
|
||||
return r.reconcileVelero(ctx, j)
|
||||
default:
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("StrategyRef.Kind not supported: %s", j.Spec.StrategyRef.Kind))
|
||||
}
|
||||
}
|
||||
|
||||
// SetupWithManager registers our controller with the Manager and sets up watches.
|
||||
func (r *BackupJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
cfg := mgr.GetConfig()
|
||||
var err error
|
||||
if r.Interface, err = dynamic.NewForConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
var h *http.Client
|
||||
if h, err = rest.HTTPClientFor(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RESTMapper, err = apiutil.NewDynamicRESTMapper(cfg, h); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&backupsv1alpha1.BackupJob{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) markBackupJobFailed(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, message string) (ctrl.Result, error) {
|
||||
logger := getLogger(ctx)
|
||||
now := metav1.Now()
|
||||
backupJob.Status.CompletedAt = &now
|
||||
backupJob.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed
|
||||
backupJob.Status.Message = message
|
||||
|
||||
// Add condition
|
||||
backupJob.Status.Conditions = append(backupJob.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "BackupFailed",
|
||||
Message: message,
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
if err := r.Status().Update(ctx, backupJob); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status to Failed")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("BackupJob failed", "message", message)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
28
internal/backupcontroller/factory/backupjob.go
Normal file
28
internal/backupcontroller/factory/backupjob.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func BackupJob(p *backupsv1alpha1.Plan, scheduledFor time.Time) *backupsv1alpha1.BackupJob {
|
||||
job := &backupsv1alpha1.BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s-%d", p.Name, scheduledFor.Unix()/60),
|
||||
Namespace: p.Namespace,
|
||||
},
|
||||
Spec: backupsv1alpha1.BackupJobSpec{
|
||||
PlanRef: &corev1.LocalObjectReference{
|
||||
Name: p.Name,
|
||||
},
|
||||
ApplicationRef: *p.Spec.ApplicationRef.DeepCopy(),
|
||||
StorageRef: *p.Spec.StorageRef.DeepCopy(),
|
||||
StrategyRef: *p.Spec.StrategyRef.DeepCopy(),
|
||||
},
|
||||
}
|
||||
return job
|
||||
}
|
||||
20
internal/backupcontroller/jobstrategy_controller.go
Normal file
20
internal/backupcontroller/jobstrategy_controller.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
)
|
||||
|
||||
func (r *BackupJobReconciler) reconcileJob(ctx context.Context, j *backupsv1alpha1.BackupJob) (ctrl.Result, error) {
|
||||
_ = log.FromContext(ctx)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *RestoreJobReconciler) reconcileJobRestore(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) (ctrl.Result, error) {
|
||||
_ = log.FromContext(ctx)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
104
internal/backupcontroller/plan_controller.go
Normal file
104
internal/backupcontroller/plan_controller.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cron "github.com/robfig/cron/v3"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/backupcontroller/factory"
|
||||
)
|
||||
|
||||
const (
|
||||
minRequeueDelay = 30 * time.Second
|
||||
startingDeadlineSeconds = 300 * time.Second
|
||||
)
|
||||
|
||||
// PlanReconciler reconciles a Plan object
|
||||
type PlanReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (r *PlanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := log.FromContext(ctx)
|
||||
|
||||
log.V(2).Info("reconciling")
|
||||
|
||||
p := &backupsv1alpha1.Plan{}
|
||||
|
||||
if err := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, p); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
log.V(3).Info("Plan not found")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
tCheck := time.Now().Add(-startingDeadlineSeconds)
|
||||
sch, err := cron.ParseStandard(p.Spec.Schedule.Cron)
|
||||
if err != nil {
|
||||
errWrapped := fmt.Errorf("could not parse cron %s: %w", p.Spec.Schedule.Cron, err)
|
||||
log.Error(err, "could not parse cron", "cron", p.Spec.Schedule.Cron)
|
||||
meta.SetStatusCondition(&p.Status.Conditions, metav1.Condition{
|
||||
Type: backupsv1alpha1.PlanConditionError,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Failed to parse cron spec",
|
||||
Message: errWrapped.Error(),
|
||||
})
|
||||
if err := r.Status().Update(ctx, p); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Clear error condition if cron parsing succeeds
|
||||
if condition := meta.FindStatusCondition(p.Status.Conditions, backupsv1alpha1.PlanConditionError); condition != nil && condition.Status == metav1.ConditionTrue {
|
||||
meta.SetStatusCondition(&p.Status.Conditions, metav1.Condition{
|
||||
Type: backupsv1alpha1.PlanConditionError,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "Cron spec is valid",
|
||||
Message: "The cron schedule has been successfully parsed",
|
||||
})
|
||||
if err := r.Status().Update(ctx, p); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
tNext := sch.Next(tCheck)
|
||||
|
||||
if time.Now().Before(tNext) {
|
||||
return ctrl.Result{RequeueAfter: tNext.Sub(time.Now())}, nil
|
||||
}
|
||||
|
||||
job := factory.BackupJob(p, tNext)
|
||||
if err := controllerutil.SetControllerReference(p, job, r.Scheme); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, job); err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
return ctrl.Result{RequeueAfter: startingDeadlineSeconds}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{RequeueAfter: startingDeadlineSeconds}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager registers our controller with the Manager and sets up watches.
|
||||
func (r *PlanReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&backupsv1alpha1.Plan{}).
|
||||
Complete(r)
|
||||
}
|
||||
139
internal/backupcontroller/restorejob_controller.go
Normal file
139
internal/backupcontroller/restorejob_controller.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
)
|
||||
|
||||
// RestoreJobReconciler reconciles RestoreJob objects.
|
||||
// It routes RestoreJobs to strategy-specific handlers based on the strategy
|
||||
// referenced in the Backup that the RestoreJob is restoring from.
|
||||
type RestoreJobReconciler struct {
|
||||
client.Client
|
||||
dynamic.Interface
|
||||
meta.RESTMapper
|
||||
Scheme *runtime.Scheme
|
||||
Recorder record.EventRecorder
|
||||
}
|
||||
|
||||
func (r *RestoreJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("reconciling RestoreJob", "namespace", req.Namespace, "name", req.Name)
|
||||
|
||||
restoreJob := &backupsv1alpha1.RestoreJob{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, restoreJob)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
logger.V(1).Info("RestoreJob not found, skipping")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Error(err, "failed to get RestoreJob")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// If already completed, no need to reconcile
|
||||
if restoreJob.Status.Phase == backupsv1alpha1.RestoreJobPhaseSucceeded ||
|
||||
restoreJob.Status.Phase == backupsv1alpha1.RestoreJobPhaseFailed {
|
||||
logger.V(1).Info("RestoreJob already completed, skipping", "phase", restoreJob.Status.Phase)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Step 1: Fetch the referenced Backup
|
||||
backup := &backupsv1alpha1.Backup{}
|
||||
backupKey := types.NamespacedName{Namespace: req.Namespace, Name: restoreJob.Spec.BackupRef.Name}
|
||||
if err := r.Get(ctx, backupKey, backup); err != nil {
|
||||
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("failed to get Backup: %v", err))
|
||||
}
|
||||
|
||||
// Step 2: Determine effective strategy from backup.spec.strategyRef
|
||||
if backup.Spec.StrategyRef.APIGroup == nil {
|
||||
return r.markRestoreJobFailed(ctx, restoreJob, "Backup has nil StrategyRef.APIGroup")
|
||||
}
|
||||
|
||||
if *backup.Spec.StrategyRef.APIGroup != strategyv1alpha1.GroupVersion.Group {
|
||||
return r.markRestoreJobFailed(ctx, restoreJob,
|
||||
fmt.Sprintf("StrategyRef.APIGroup doesn't match: %s", *backup.Spec.StrategyRef.APIGroup))
|
||||
}
|
||||
|
||||
logger.Info("processing RestoreJob", "restorejob", restoreJob.Name, "backup", backup.Name, "strategyKind", backup.Spec.StrategyRef.Kind)
|
||||
switch backup.Spec.StrategyRef.Kind {
|
||||
case strategyv1alpha1.JobStrategyKind:
|
||||
return r.reconcileJobRestore(ctx, restoreJob, backup)
|
||||
case strategyv1alpha1.VeleroStrategyKind:
|
||||
return r.reconcileVeleroRestore(ctx, restoreJob, backup)
|
||||
default:
|
||||
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("StrategyRef.Kind not supported: %s", backup.Spec.StrategyRef.Kind))
|
||||
}
|
||||
}
|
||||
|
||||
// SetupWithManager registers our controller with the Manager and sets up watches.
|
||||
func (r *RestoreJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
cfg := mgr.GetConfig()
|
||||
var err error
|
||||
if r.Interface, err = dynamic.NewForConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
var h *http.Client
|
||||
if h, err = rest.HTTPClientFor(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RESTMapper, err = apiutil.NewDynamicRESTMapper(cfg, h); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&backupsv1alpha1.RestoreJob{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
// getTargetApplicationRef determines the effective target application reference.
|
||||
// According to DESIGN.md, if spec.targetApplicationRef is omitted, drivers SHOULD
|
||||
// restore into backup.spec.applicationRef.
|
||||
func (r *RestoreJobReconciler) getTargetApplicationRef(restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) corev1.TypedLocalObjectReference {
|
||||
if restoreJob.Spec.TargetApplicationRef != nil {
|
||||
return *restoreJob.Spec.TargetApplicationRef
|
||||
}
|
||||
return backup.Spec.ApplicationRef
|
||||
}
|
||||
|
||||
// markRestoreJobFailed updates the RestoreJob status to Failed with the given message.
|
||||
func (r *RestoreJobReconciler) markRestoreJobFailed(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob, message string) (ctrl.Result, error) {
|
||||
logger := getLogger(ctx)
|
||||
now := metav1.Now()
|
||||
restoreJob.Status.CompletedAt = &now
|
||||
restoreJob.Status.Phase = backupsv1alpha1.RestoreJobPhaseFailed
|
||||
restoreJob.Status.Message = message
|
||||
|
||||
// Add condition
|
||||
restoreJob.Status.Conditions = append(restoreJob.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "RestoreFailed",
|
||||
Message: message,
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
if err := r.Status().Update(ctx, restoreJob); err != nil {
|
||||
logger.Error(err, "failed to update RestoreJob status to Failed")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("RestoreJob failed", "message", message)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
771
internal/backupcontroller/velerostrategy_controller.go
Normal file
771
internal/backupcontroller/velerostrategy_controller.go
Normal file
@@ -0,0 +1,771 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/template"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
func getLogger(ctx context.Context) loggerWithDebug {
|
||||
return loggerWithDebug{Logger: log.FromContext(ctx)}
|
||||
}
|
||||
|
||||
// loggerWithDebug wraps a logr.Logger and provides a Debug() method
|
||||
// that maps to V(1).Info() for convenience.
|
||||
type loggerWithDebug struct {
|
||||
logr.Logger
|
||||
}
|
||||
|
||||
// Debug logs at debug level (equivalent to V(1).Info())
|
||||
func (l loggerWithDebug) Debug(msg string, keysAndValues ...interface{}) {
|
||||
l.Logger.V(1).Info(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// S3Credentials holds the discovered S3 credentials from a Bucket storageRef
|
||||
type S3Credentials struct {
|
||||
BucketName string
|
||||
Endpoint string
|
||||
Region string
|
||||
AccessKeyID string
|
||||
AccessSecretKey string
|
||||
}
|
||||
|
||||
// bucketInfo represents the structure of BucketInfo stored in the secret
|
||||
type bucketInfo struct {
|
||||
Spec struct {
|
||||
BucketName string `json:"bucketName"`
|
||||
SecretS3 struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
AccessKeyID string `json:"accessKeyID"`
|
||||
AccessSecretKey string `json:"accessSecretKey"`
|
||||
} `json:"secretS3"`
|
||||
} `json:"spec"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRequeueAfter = 5 * time.Second
|
||||
defaultActiveJobPollingInterval = defaultRequeueAfter
|
||||
defaultRestoreRequeueAfter = 5 * time.Second
|
||||
defaultActiveRestorePollingInterval = defaultRestoreRequeueAfter
|
||||
// Velero requires API objects and secrets to be in the cozy-velero namespace
|
||||
veleroNamespace = "cozy-velero"
|
||||
veleroBackupNameMetadataKey = "velero.io/backup-name"
|
||||
veleroBackupNamespaceMetadataKey = "velero.io/backup-namespace"
|
||||
)
|
||||
|
||||
func storageS3SecretName(namespace, backupJobName string) string {
|
||||
return fmt.Sprintf("backup-%s-%s-s3-credentials", namespace, backupJobName)
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) reconcileVelero(ctx context.Context, j *backupsv1alpha1.BackupJob) (ctrl.Result, error) {
|
||||
logger := getLogger(ctx)
|
||||
logger.Debug("reconciling Velero strategy", "backupjob", j.Name, "phase", j.Status.Phase)
|
||||
|
||||
// If already completed, no need to reconcile
|
||||
if j.Status.Phase == backupsv1alpha1.BackupJobPhaseSucceeded ||
|
||||
j.Status.Phase == backupsv1alpha1.BackupJobPhaseFailed {
|
||||
logger.Debug("BackupJob already completed, skipping", "phase", j.Status.Phase)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Step 1: On first reconcile, set startedAt (but not phase yet - phase will be set after backup creation)
|
||||
logger.Debug("checking BackupJob status", "startedAt", j.Status.StartedAt, "phase", j.Status.Phase)
|
||||
if j.Status.StartedAt == nil {
|
||||
logger.Debug("setting BackupJob StartedAt")
|
||||
now := metav1.Now()
|
||||
j.Status.StartedAt = &now
|
||||
// Don't set phase to Running yet - will be set after Velero backup is successfully created
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("set BackupJob StartedAt", "startedAt", j.Status.StartedAt)
|
||||
} else {
|
||||
logger.Debug("BackupJob already started", "startedAt", j.Status.StartedAt, "phase", j.Status.Phase)
|
||||
}
|
||||
|
||||
// Step 2: Resolve inputs - Read Strategy, Storage, Application, optionally Plan
|
||||
logger.Debug("fetching Velero strategy", "strategyName", j.Spec.StrategyRef.Name)
|
||||
veleroStrategy := &strategyv1alpha1.Velero{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: j.Spec.StrategyRef.Name}, veleroStrategy); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Error(err, "Velero strategy not found", "strategyName", j.Spec.StrategyRef.Name)
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("Velero strategy not found: %s", j.Spec.StrategyRef.Name))
|
||||
}
|
||||
logger.Error(err, "failed to get Velero strategy")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("fetched Velero strategy", "strategyName", veleroStrategy.Name)
|
||||
|
||||
// Step 3: Execute backup logic
|
||||
// Check if we already created a Velero Backup
|
||||
if j.Status.StartedAt == nil {
|
||||
logger.Error(nil, "StartedAt is nil after status update, this should not happen")
|
||||
return ctrl.Result{RequeueAfter: defaultRequeueAfter}, nil
|
||||
}
|
||||
logger.Debug("checking for existing Velero Backup", "namespace", veleroNamespace)
|
||||
veleroBackupList := &velerov1.BackupList{}
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(veleroNamespace),
|
||||
client.MatchingLabels{
|
||||
backupsv1alpha1.OwningJobNamespaceLabel: j.Namespace,
|
||||
backupsv1alpha1.OwningJobNameLabel: j.Name,
|
||||
},
|
||||
}
|
||||
|
||||
if err := r.List(ctx, veleroBackupList, opts...); err != nil {
|
||||
logger.Error(err, "failed to get Velero Backup")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if len(veleroBackupList.Items) == 0 {
|
||||
// Create Velero Backup
|
||||
logger.Debug("Velero Backup not found, creating new one")
|
||||
if err := r.createVeleroBackup(ctx, j, veleroStrategy); err != nil {
|
||||
logger.Error(err, "failed to create Velero Backup")
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to create Velero Backup: %v", err))
|
||||
}
|
||||
// After successful Velero backup creation, set phase to Running
|
||||
if j.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
|
||||
logger.Debug("setting BackupJob phase to Running after successful Velero backup creation")
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseRunning
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob phase to Running")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
logger.Debug("created Velero Backup, requeuing")
|
||||
// Requeue to check status
|
||||
return ctrl.Result{RequeueAfter: defaultRequeueAfter}, nil
|
||||
}
|
||||
|
||||
if len(veleroBackupList.Items) > 1 {
|
||||
logger.Error(fmt.Errorf("too many Velero backups for BackupJob"), "found more than one Velero Backup referencing a single BackupJob as owner")
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status")
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
veleroBackup := veleroBackupList.Items[0].DeepCopy()
|
||||
logger.Debug("found existing Velero Backup", "phase", veleroBackup.Status.Phase)
|
||||
|
||||
// If Velero backup exists but phase is not Running, set it to Running
|
||||
// This handles the case where the backup was created but phase wasn't set yet
|
||||
if j.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
|
||||
logger.Debug("setting BackupJob phase to Running (Velero backup already exists)")
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseRunning
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob phase to Running")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check Velero Backup status
|
||||
phase := string(veleroBackup.Status.Phase)
|
||||
if phase == "" {
|
||||
// Still in progress, requeue
|
||||
return ctrl.Result{RequeueAfter: defaultActiveJobPollingInterval}, nil
|
||||
}
|
||||
|
||||
// Step 4: On success - Create Backup resource and update status
|
||||
if phase == "Completed" {
|
||||
// Check if we already created the Backup resource
|
||||
if j.Status.BackupRef == nil {
|
||||
backup, err := r.createBackupResource(ctx, j, veleroBackup)
|
||||
if err != nil {
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to create Backup resource: %v", err))
|
||||
}
|
||||
|
||||
now := metav1.Now()
|
||||
j.Status.BackupRef = &corev1.LocalObjectReference{Name: backup.Name}
|
||||
j.Status.CompletedAt = &now
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseSucceeded
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("BackupJob succeeded", "backup", backup.Name)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Step 5: On failure
|
||||
if phase == "Failed" || phase == "PartiallyFailed" {
|
||||
message := fmt.Sprintf("Velero Backup failed with phase: %s", phase)
|
||||
if len(veleroBackup.Status.ValidationErrors) > 0 {
|
||||
message = fmt.Sprintf("%s: %v", message, veleroBackup.Status.ValidationErrors)
|
||||
}
|
||||
return r.markBackupJobFailed(ctx, j, message)
|
||||
}
|
||||
|
||||
// Still in progress (InProgress, New, etc.)
|
||||
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||
}
|
||||
|
||||
// resolveBucketStorageRef discovers S3 credentials from a Bucket storageRef
|
||||
// It follows this flow:
|
||||
// 1. Get the Bucket resource (apps.cozystack.io/v1alpha1)
|
||||
// 2. Find the BucketAccess that references this bucket
|
||||
// 3. Get the secret from BucketAccess.spec.credentialsSecretName
|
||||
// 4. Decode BucketInfo from secret.data.BucketInfo and extract S3 credentials
|
||||
func (r *BackupJobReconciler) resolveBucketStorageRef(ctx context.Context, storageRef corev1.TypedLocalObjectReference, namespace string) (*S3Credentials, error) {
|
||||
logger := getLogger(ctx)
|
||||
|
||||
// Step 1: Get the Bucket resource
|
||||
bucket := &unstructured.Unstructured{}
|
||||
bucket.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: *storageRef.APIGroup,
|
||||
Version: "v1alpha1",
|
||||
Kind: storageRef.Kind,
|
||||
})
|
||||
|
||||
if *storageRef.APIGroup != "apps.cozystack.io" {
|
||||
return nil, fmt.Errorf("Unsupported storage APIGroup: %v, expected apps.cozystack.io", storageRef.APIGroup)
|
||||
}
|
||||
bucketKey := client.ObjectKey{Namespace: namespace, Name: storageRef.Name}
|
||||
|
||||
if err := r.Get(ctx, bucketKey, bucket); err != nil {
|
||||
return nil, fmt.Errorf("failed to get Bucket %s: %w", storageRef.Name, err)
|
||||
}
|
||||
|
||||
// Step 2: Determine the bucket claim name
|
||||
// For apps.cozystack.io Bucket, the BucketClaim name is typically the same as the Bucket name
|
||||
// or follows a pattern. Based on the templates, it's usually the Release.Name which equals the Bucket name
|
||||
bucketName := storageRef.Name
|
||||
|
||||
// Step 3: Get BucketAccess by name (assuming BucketAccess name matches bucketName)
|
||||
bucketAccess := &unstructured.Unstructured{}
|
||||
bucketAccess.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: "objectstorage.k8s.io",
|
||||
Version: "v1alpha1",
|
||||
Kind: "BucketAccess",
|
||||
})
|
||||
|
||||
bucketAccessKey := client.ObjectKey{Name: "bucket-" + bucketName, Namespace: namespace}
|
||||
if err := r.Get(ctx, bucketAccessKey, bucketAccess); err != nil {
|
||||
return nil, fmt.Errorf("failed to get BucketAccess %s in namespace %s: %w", bucketName, namespace, err)
|
||||
}
|
||||
|
||||
// Step 4: Get the secret name from BucketAccess
|
||||
secretName, found, err := unstructured.NestedString(bucketAccess.Object, "spec", "credentialsSecretName")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get credentialsSecretName from BucketAccess: %w", err)
|
||||
}
|
||||
if !found || secretName == "" {
|
||||
return nil, fmt.Errorf("credentialsSecretName not found in BucketAccess %s", bucketAccessKey.Name)
|
||||
}
|
||||
|
||||
// Step 5: Get the secret
|
||||
secret := &corev1.Secret{}
|
||||
secretKey := client.ObjectKey{Namespace: namespace, Name: secretName}
|
||||
if err := r.Get(ctx, secretKey, secret); err != nil {
|
||||
return nil, fmt.Errorf("failed to get secret %s: %w", secretName, err)
|
||||
}
|
||||
|
||||
// Step 6: Decode BucketInfo from secret.data.BucketInfo
|
||||
bucketInfoData, found := secret.Data["BucketInfo"]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("BucketInfo key not found in secret %s", secretName)
|
||||
}
|
||||
|
||||
// Parse JSON value
|
||||
var info bucketInfo
|
||||
if err := json.Unmarshal(bucketInfoData, &info); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal BucketInfo from secret %s: %w", secretName, err)
|
||||
}
|
||||
|
||||
// Step 7: Extract and return S3 credentials
|
||||
creds := &S3Credentials{
|
||||
BucketName: info.Spec.BucketName,
|
||||
Endpoint: info.Spec.SecretS3.Endpoint,
|
||||
Region: info.Spec.SecretS3.Region,
|
||||
AccessKeyID: info.Spec.SecretS3.AccessKeyID,
|
||||
AccessSecretKey: info.Spec.SecretS3.AccessSecretKey,
|
||||
}
|
||||
|
||||
logger.Debug("resolved S3 credentials from Bucket storageRef",
|
||||
"bucket", storageRef.Name,
|
||||
"bucketName", creds.BucketName,
|
||||
"endpoint", creds.Endpoint)
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// createS3CredsForVelero creates or updates a Kubernetes Secret containing
|
||||
// Velero S3 credentials in the format expected by Velero's cloud-credentials plugin.
|
||||
func (r *BackupJobReconciler) createS3CredsForVelero(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, creds *S3Credentials) error {
|
||||
logger := getLogger(ctx)
|
||||
secretName := storageS3SecretName(backupJob.Namespace, backupJob.Name)
|
||||
secretNamespace := veleroNamespace
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: secretNamespace,
|
||||
},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
StringData: map[string]string{
|
||||
"cloud": fmt.Sprintf(`[default]
|
||||
aws_access_key_id=%s
|
||||
aws_secret_access_key=%s
|
||||
|
||||
services = seaweed-s3
|
||||
[services seaweed-s3]
|
||||
s3 =
|
||||
endpoint_url = %s
|
||||
`, creds.AccessKeyID, creds.AccessSecretKey, creds.Endpoint),
|
||||
},
|
||||
}
|
||||
|
||||
foundSecret := &corev1.Secret{}
|
||||
secretKey := client.ObjectKey{Name: secretName, Namespace: secretNamespace}
|
||||
err := r.Get(ctx, secretKey, foundSecret)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// Create the Secret
|
||||
if err := r.Create(ctx, secret); err != nil {
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "SecretCreationFailed",
|
||||
fmt.Sprintf("Failed to create Velero credentials secret %s/%s: %v", secretNamespace, secretName, err))
|
||||
return fmt.Errorf("failed to create Velero credentials secret: %w", err)
|
||||
}
|
||||
logger.Debug("created Velero credentials secret", "secret", secretName)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "SecretCreated",
|
||||
fmt.Sprintf("Created Velero credentials secret %s/%s", secretNamespace, secretName))
|
||||
} else if err == nil {
|
||||
// Update if necessary - only update if the secret data has actually changed
|
||||
// Compare the new secret data with existing secret data
|
||||
existingData := foundSecret.Data
|
||||
if existingData == nil {
|
||||
existingData = make(map[string][]byte)
|
||||
}
|
||||
newData := make(map[string][]byte)
|
||||
for k, v := range secret.StringData {
|
||||
newData[k] = []byte(v)
|
||||
}
|
||||
|
||||
// Check if data has changed
|
||||
dataChanged := false
|
||||
if len(existingData) != len(newData) {
|
||||
dataChanged = true
|
||||
} else {
|
||||
for k, newVal := range newData {
|
||||
existingVal, exists := existingData[k]
|
||||
if !exists || !reflect.DeepEqual(existingVal, newVal) {
|
||||
dataChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dataChanged {
|
||||
foundSecret.StringData = secret.StringData
|
||||
foundSecret.Data = nil // Clear .Data so .StringData will be used
|
||||
if err := r.Update(ctx, foundSecret); err != nil {
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "SecretUpdateFailed",
|
||||
fmt.Sprintf("Failed to update Velero credentials secret %s/%s: %v", secretNamespace, secretName, err))
|
||||
return fmt.Errorf("failed to update Velero credentials secret: %w", err)
|
||||
}
|
||||
logger.Debug("updated Velero credentials secret", "secret", secretName)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "SecretUpdated",
|
||||
fmt.Sprintf("Updated Velero credentials secret %s/%s", secretNamespace, secretName))
|
||||
} else {
|
||||
logger.Debug("Velero credentials secret data unchanged, skipping update", "secret", secretName)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for existing Velero credentials secret: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createBackupStorageLocation creates or updates a Velero BackupStorageLocation resource.
|
||||
func (r *BackupJobReconciler) createBackupStorageLocation(ctx context.Context, bsl *velerov1.BackupStorageLocation) error {
|
||||
logger := getLogger(ctx)
|
||||
foundBSL := &velerov1.BackupStorageLocation{}
|
||||
bslKey := client.ObjectKey{Name: bsl.Name, Namespace: bsl.Namespace}
|
||||
|
||||
err := r.Get(ctx, bslKey, foundBSL)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// Create the BackupStorageLocation
|
||||
if err := r.Create(ctx, bsl); err != nil {
|
||||
return fmt.Errorf("failed to create BackupStorageLocation: %w", err)
|
||||
}
|
||||
logger.Debug("created BackupStorageLocation", "name", bsl.Name, "namespace", bsl.Namespace)
|
||||
} else if err == nil {
|
||||
// Update if necessary - use patch to avoid conflicts with Velero's status updates
|
||||
// Only update if the spec has actually changed
|
||||
if !reflect.DeepEqual(foundBSL.Spec, bsl.Spec) {
|
||||
// Retry on conflict since Velero may be updating status concurrently
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := r.Get(ctx, bslKey, foundBSL); err != nil {
|
||||
return fmt.Errorf("failed to get BackupStorageLocation for update: %w", err)
|
||||
}
|
||||
foundBSL.Spec = bsl.Spec
|
||||
if err := r.Update(ctx, foundBSL); err != nil {
|
||||
if errors.IsConflict(err) && i < 2 {
|
||||
logger.Debug("conflict updating BackupStorageLocation, retrying", "attempt", i+1)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to update BackupStorageLocation: %w", err)
|
||||
}
|
||||
logger.Debug("updated BackupStorageLocation", "name", bsl.Name, "namespace", bsl.Namespace)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
logger.Debug("BackupStorageLocation spec unchanged, skipping update", "name", bsl.Name, "namespace", bsl.Namespace)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for existing BackupStorageLocation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createVolumeSnapshotLocation creates or updates a Velero VolumeSnapshotLocation resource.
|
||||
func (r *BackupJobReconciler) createVolumeSnapshotLocation(ctx context.Context, vsl *velerov1.VolumeSnapshotLocation) error {
|
||||
logger := getLogger(ctx)
|
||||
foundVSL := &velerov1.VolumeSnapshotLocation{}
|
||||
vslKey := client.ObjectKey{Name: vsl.Name, Namespace: vsl.Namespace}
|
||||
|
||||
err := r.Get(ctx, vslKey, foundVSL)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// Create the VolumeSnapshotLocation
|
||||
if err := r.Create(ctx, vsl); err != nil {
|
||||
return fmt.Errorf("failed to create VolumeSnapshotLocation: %w", err)
|
||||
}
|
||||
logger.Debug("created VolumeSnapshotLocation", "name", vsl.Name, "namespace", vsl.Namespace)
|
||||
} else if err == nil {
|
||||
// Update if necessary - only update if the spec has actually changed
|
||||
if !reflect.DeepEqual(foundVSL.Spec, vsl.Spec) {
|
||||
// Retry on conflict since Velero may be updating status concurrently
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := r.Get(ctx, vslKey, foundVSL); err != nil {
|
||||
return fmt.Errorf("failed to get VolumeSnapshotLocation for update: %w", err)
|
||||
}
|
||||
foundVSL.Spec = vsl.Spec
|
||||
if err := r.Update(ctx, foundVSL); err != nil {
|
||||
if errors.IsConflict(err) && i < 2 {
|
||||
logger.Debug("conflict updating VolumeSnapshotLocation, retrying", "attempt", i+1)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to update VolumeSnapshotLocation: %w", err)
|
||||
}
|
||||
logger.Debug("updated VolumeSnapshotLocation", "name", vsl.Name, "namespace", vsl.Namespace)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
logger.Debug("VolumeSnapshotLocation spec unchanged, skipping update", "name", vsl.Name, "namespace", vsl.Namespace)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for existing VolumeSnapshotLocation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) createVeleroBackup(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, strategy *strategyv1alpha1.Velero) error {
|
||||
logger := getLogger(ctx)
|
||||
logger.Debug("createVeleroBackup called", "strategy", strategy.Name)
|
||||
|
||||
mapping, err := r.RESTMapping(schema.GroupKind{Group: *backupJob.Spec.ApplicationRef.APIGroup, Kind: backupJob.Spec.ApplicationRef.Kind})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ns := backupJob.Namespace
|
||||
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
|
||||
ns = ""
|
||||
}
|
||||
app, err := r.Resource(mapping.Resource).Namespace(ns).Get(ctx, backupJob.Spec.ApplicationRef.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
veleroBackupSpec, err := template.Template(&strategy.Spec.Template.Spec, app.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
veleroBackup := &velerov1.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: fmt.Sprintf("%s.%s-", backupJob.Namespace, backupJob.Name),
|
||||
Namespace: veleroNamespace,
|
||||
Labels: map[string]string{
|
||||
backupsv1alpha1.OwningJobNameLabel: backupJob.Name,
|
||||
backupsv1alpha1.OwningJobNamespaceLabel: backupJob.Namespace,
|
||||
},
|
||||
},
|
||||
Spec: *veleroBackupSpec,
|
||||
}
|
||||
name := veleroBackup.GenerateName
|
||||
if err := r.Create(ctx, veleroBackup); err != nil {
|
||||
if veleroBackup.Name != "" {
|
||||
name = veleroBackup.Name
|
||||
}
|
||||
logger.Error(err, "failed to create Velero Backup", "name", veleroBackup.Name)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "VeleroBackupCreationFailed",
|
||||
fmt.Sprintf("Failed to create Velero Backup %s/%s: %v", veleroNamespace, name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("created Velero Backup", "name", veleroBackup.Name, "namespace", veleroBackup.Namespace)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "VeleroBackupCreated",
|
||||
fmt.Sprintf("Created Velero Backup %s/%s", veleroNamespace, name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) createBackupResource(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, veleroBackup *velerov1.Backup) (*backupsv1alpha1.Backup, error) {
|
||||
logger := getLogger(ctx)
|
||||
// Extract artifact information from Velero Backup
|
||||
// Create a basic artifact referencing the Velero backup
|
||||
artifact := &backupsv1alpha1.BackupArtifact{
|
||||
URI: fmt.Sprintf("velero://%s/%s", backupJob.Namespace, veleroBackup.Name),
|
||||
}
|
||||
|
||||
// Get takenAt from Velero Backup creation timestamp or status
|
||||
takenAt := metav1.Now()
|
||||
if veleroBackup.Status.StartTimestamp != nil {
|
||||
takenAt = *veleroBackup.Status.StartTimestamp
|
||||
} else if !veleroBackup.CreationTimestamp.IsZero() {
|
||||
takenAt = veleroBackup.CreationTimestamp
|
||||
}
|
||||
|
||||
// Extract driver metadata (e.g., Velero backup name)
|
||||
driverMetadata := map[string]string{
|
||||
veleroBackupNameMetadataKey: veleroBackup.Name,
|
||||
veleroBackupNamespaceMetadataKey: veleroBackup.Namespace,
|
||||
}
|
||||
|
||||
backup := &backupsv1alpha1.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s", backupJob.Name),
|
||||
Namespace: backupJob.Namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: backupJob.APIVersion,
|
||||
Kind: backupJob.Kind,
|
||||
Name: backupJob.Name,
|
||||
UID: backupJob.UID,
|
||||
Controller: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: backupsv1alpha1.BackupSpec{
|
||||
ApplicationRef: backupJob.Spec.ApplicationRef,
|
||||
StorageRef: backupJob.Spec.StorageRef,
|
||||
StrategyRef: backupJob.Spec.StrategyRef,
|
||||
TakenAt: takenAt,
|
||||
DriverMetadata: driverMetadata,
|
||||
},
|
||||
Status: backupsv1alpha1.BackupStatus{
|
||||
Phase: backupsv1alpha1.BackupPhaseReady,
|
||||
},
|
||||
}
|
||||
|
||||
if backupJob.Spec.PlanRef != nil {
|
||||
backup.Spec.PlanRef = backupJob.Spec.PlanRef
|
||||
}
|
||||
|
||||
if artifact != nil {
|
||||
backup.Status.Artifact = artifact
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, backup); err != nil {
|
||||
logger.Error(err, "failed to create Backup resource")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug("created Backup resource", "name", backup.Name)
|
||||
return backup, nil
|
||||
}
|
||||
|
||||
// reconcileVeleroRestore handles restore operations for Velero strategy.
|
||||
func (r *RestoreJobReconciler) reconcileVeleroRestore(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup) (ctrl.Result, error) {
|
||||
logger := getLogger(ctx)
|
||||
logger.Debug("reconciling Velero strategy restore", "restorejob", restoreJob.Name, "backup", backup.Name)
|
||||
|
||||
// Step 1: On first reconcile, set startedAt and phase = Running
|
||||
if restoreJob.Status.StartedAt == nil {
|
||||
logger.Debug("setting RestoreJob StartedAt and phase to Running")
|
||||
now := metav1.Now()
|
||||
restoreJob.Status.StartedAt = &now
|
||||
restoreJob.Status.Phase = backupsv1alpha1.RestoreJobPhaseRunning
|
||||
if err := r.Status().Update(ctx, restoreJob); err != nil {
|
||||
logger.Error(err, "failed to update RestoreJob status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{RequeueAfter: defaultRestoreRequeueAfter}, nil
|
||||
}
|
||||
|
||||
// Step 2: Resolve inputs - Read Strategy, Storage, target Application
|
||||
logger.Debug("fetching Velero strategy", "strategyName", backup.Spec.StrategyRef.Name)
|
||||
veleroStrategy := &strategyv1alpha1.Velero{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: backup.Spec.StrategyRef.Name}, veleroStrategy); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Error(err, "Velero strategy not found", "strategyName", backup.Spec.StrategyRef.Name)
|
||||
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("Velero strategy not found: %s", backup.Spec.StrategyRef.Name))
|
||||
}
|
||||
logger.Error(err, "failed to get Velero strategy")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("fetched Velero strategy", "strategyName", veleroStrategy.Name)
|
||||
|
||||
// Get Velero backup name from Backup's driverMetadata
|
||||
veleroBackupName, ok := backup.Spec.DriverMetadata[veleroBackupNameMetadataKey]
|
||||
if !ok {
|
||||
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("Backup missing Velero backup name in driverMetadata (key: %s)", veleroBackupNameMetadataKey))
|
||||
}
|
||||
|
||||
// Step 3: Execute restore logic
|
||||
// Check if we already created a Velero Restore
|
||||
logger.Debug("checking for existing Velero Restore", "namespace", veleroNamespace)
|
||||
veleroRestoreList := &velerov1.RestoreList{}
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(veleroNamespace),
|
||||
client.MatchingLabels{
|
||||
backupsv1alpha1.OwningJobNameLabel: restoreJob.Name,
|
||||
backupsv1alpha1.OwningJobNamespaceLabel: restoreJob.Namespace,
|
||||
},
|
||||
}
|
||||
|
||||
if err := r.List(ctx, veleroRestoreList, opts...); err != nil {
|
||||
logger.Error(err, "failed to get Velero Restore")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if len(veleroRestoreList.Items) == 0 {
|
||||
// Create Velero Restore
|
||||
logger.Debug("Velero Restore not found, creating new one")
|
||||
if err := r.createVeleroRestore(ctx, restoreJob, backup, veleroStrategy, veleroBackupName); err != nil {
|
||||
logger.Error(err, "failed to create Velero Restore")
|
||||
return r.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("failed to create Velero Restore: %v", err))
|
||||
}
|
||||
logger.Debug("created Velero Restore, requeuing")
|
||||
// Requeue to check status
|
||||
return ctrl.Result{RequeueAfter: defaultRestoreRequeueAfter}, nil
|
||||
}
|
||||
|
||||
if len(veleroRestoreList.Items) > 1 {
|
||||
logger.Error(fmt.Errorf("too many Velero restores for RestoreJob"), "found more than one Velero Restore referencing a single RestoreJob as owner")
|
||||
return r.markRestoreJobFailed(ctx, restoreJob, "found multiple Velero Restores for this RestoreJob")
|
||||
}
|
||||
|
||||
veleroRestore := veleroRestoreList.Items[0].DeepCopy()
|
||||
logger.Debug("found existing Velero Restore", "phase", veleroRestore.Status.Phase)
|
||||
|
||||
// Check Velero Restore status
|
||||
phase := string(veleroRestore.Status.Phase)
|
||||
if phase == "" {
|
||||
// Still in progress, requeue
|
||||
return ctrl.Result{RequeueAfter: defaultActiveRestorePollingInterval}, nil
|
||||
}
|
||||
|
||||
// Step 4: On success
|
||||
if phase == "Completed" {
|
||||
now := metav1.Now()
|
||||
restoreJob.Status.CompletedAt = &now
|
||||
restoreJob.Status.Phase = backupsv1alpha1.RestoreJobPhaseSucceeded
|
||||
if err := r.Status().Update(ctx, restoreJob); err != nil {
|
||||
logger.Error(err, "failed to update RestoreJob status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("RestoreJob succeeded")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Step 5: On failure
|
||||
if phase == "Failed" || phase == "PartiallyFailed" {
|
||||
message := fmt.Sprintf("Velero Restore failed with phase: %s", phase)
|
||||
if veleroRestore.Status.FailureReason != "" {
|
||||
message = fmt.Sprintf("%s: %s", message, veleroRestore.Status.FailureReason)
|
||||
}
|
||||
return r.markRestoreJobFailed(ctx, restoreJob, message)
|
||||
}
|
||||
|
||||
// Still in progress (InProgress, New, etc.)
|
||||
return ctrl.Result{RequeueAfter: defaultRestoreRequeueAfter}, nil
|
||||
}
|
||||
|
||||
// createVeleroRestore creates a Velero Restore resource.
|
||||
func (r *RestoreJobReconciler) createVeleroRestore(ctx context.Context, restoreJob *backupsv1alpha1.RestoreJob, backup *backupsv1alpha1.Backup, strategy *strategyv1alpha1.Velero, veleroBackupName string) error {
|
||||
logger := getLogger(ctx)
|
||||
logger.Debug("createVeleroRestore called", "strategy", strategy.Name, "veleroBackupName", veleroBackupName)
|
||||
|
||||
// Determine target application reference
|
||||
targetAppRef := r.getTargetApplicationRef(restoreJob, backup)
|
||||
|
||||
// Get the target application object for templating
|
||||
mapping, err := r.RESTMapping(schema.GroupKind{Group: *targetAppRef.APIGroup, Kind: targetAppRef.Kind})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get REST mapping for target application: %w", err)
|
||||
}
|
||||
ns := restoreJob.Namespace
|
||||
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
|
||||
ns = ""
|
||||
}
|
||||
app, err := r.Resource(mapping.Resource).Namespace(ns).Get(ctx, targetAppRef.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get target application: %w", err)
|
||||
}
|
||||
|
||||
// Template the restore spec from the strategy
|
||||
veleroRestoreSpec, err := template.Template(&strategy.Spec.Template.RestoreSpec, app.Object)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to template Velero Restore spec: %w", err)
|
||||
}
|
||||
|
||||
// Set the backupName in the spec (required by Velero)
|
||||
veleroRestoreSpec.BackupName = veleroBackupName
|
||||
|
||||
veleroRestore := &velerov1.Restore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: fmt.Sprintf("%s.%s-", restoreJob.Namespace, restoreJob.Name),
|
||||
Namespace: veleroNamespace,
|
||||
Labels: map[string]string{
|
||||
backupsv1alpha1.OwningJobNameLabel: restoreJob.Name,
|
||||
backupsv1alpha1.OwningJobNamespaceLabel: restoreJob.Namespace,
|
||||
},
|
||||
},
|
||||
Spec: *veleroRestoreSpec,
|
||||
}
|
||||
name := veleroRestore.GenerateName
|
||||
if err := r.Create(ctx, veleroRestore); err != nil {
|
||||
if veleroRestore.Name != "" {
|
||||
name = veleroRestore.Name
|
||||
}
|
||||
logger.Error(err, "failed to create Velero Restore", "name", veleroRestore.Name)
|
||||
r.Recorder.Event(restoreJob, corev1.EventTypeWarning, "VeleroRestoreCreationFailed",
|
||||
fmt.Sprintf("Failed to create Velero Restore %s/%s: %v", veleroNamespace, name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("created Velero Restore", "name", veleroRestore.Name, "namespace", veleroRestore.Namespace)
|
||||
r.Recorder.Event(restoreJob, corev1.EventTypeNormal, "VeleroRestoreCreated",
|
||||
fmt.Sprintf("Created Velero Restore %s/%s", veleroNamespace, name))
|
||||
return nil
|
||||
}
|
||||
355
internal/controller/dashboard/README.md
Normal file
355
internal/controller/dashboard/README.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# Dashboard Resource Integration Guide
|
||||
|
||||
This guide explains how to add a new Kubernetes resource to the Cozystack dashboard. The dashboard provides a unified interface for viewing and managing Kubernetes resources through custom table views, detail pages, and sidebar navigation.
|
||||
|
||||
## Overview
|
||||
|
||||
Adding a new resource to the dashboard requires three main components:
|
||||
|
||||
1. **CustomColumnsOverride**: Defines how the resource appears in list/table views
|
||||
2. **Factory**: Defines the detail page layout for individual resource instances
|
||||
3. **Sidebar Entry**: Adds navigation to the resource in the sidebar menu
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The resource must have a Kubernetes CustomResourceDefinition (CRD) or be a built-in Kubernetes resource
|
||||
- Know the resource's API group, version, kind, and plural name
|
||||
- Understand the resource's spec structure to display relevant fields
|
||||
|
||||
## Step-by-Step Guide
|
||||
|
||||
### Step 1: Add CustomColumnsOverride
|
||||
|
||||
The CustomColumnsOverride defines the columns shown in the resource list table and how clicking on a row navigates to the detail page.
|
||||
|
||||
**Location**: `internal/controller/dashboard/static_refactored.go` in `CreateAllCustomColumnsOverrides()`
|
||||
|
||||
**Example**:
|
||||
```go
|
||||
// Stock namespace backups cozystack io v1alpha1 plans
|
||||
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/plans", []any{
|
||||
createCustomColumnWithJsonPath("Name", ".metadata.name", "Plan", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/plan-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
|
||||
createApplicationRefColumn("Application"),
|
||||
createTimestampColumn("Created", ".metadata.creationTimestamp"),
|
||||
}),
|
||||
```
|
||||
|
||||
**Key Components**:
|
||||
- **ID Format**: `stock-namespace-/{group}/{version}/{plural}`
|
||||
- **Name Column**: Use `createCustomColumnWithJsonPath()` with:
|
||||
- Badge value: Resource kind in PascalCase (e.g., "Plan", "Service")
|
||||
- Link href: `/openapi-ui/{2}/{namespace}/factory/{resource-details}/{name}`
|
||||
- **Additional Columns**: Use helper functions like:
|
||||
- `createStringColumn()`: Simple string values
|
||||
- `createTimestampColumn()`: Timestamp fields
|
||||
- `createReadyColumn()`: Ready status from conditions
|
||||
- Custom helpers for complex fields
|
||||
|
||||
**Helper Functions Available**:
|
||||
- `createCustomColumnWithJsonPath(name, jsonPath, badgeValue, badgeColor, linkHref)`: Column with badge and link
|
||||
- `createStringColumn(name, jsonPath)`: Simple string column
|
||||
- `createTimestampColumn(name, jsonPath)`: Timestamp with formatting
|
||||
- `createReadyColumn()`: Ready status column
|
||||
- `createBoolColumn(name, jsonPath)`: Boolean column
|
||||
- `createArrayColumn(name, jsonPath)`: Array column
|
||||
|
||||
### Step 2: Add Factory (Detail Page)
|
||||
|
||||
The Factory defines the detail page layout when viewing an individual resource instance.
|
||||
|
||||
**Location**: `internal/controller/dashboard/static_refactored.go` in `CreateAllFactories()`
|
||||
|
||||
**Using Unified Factory Approach** (Recommended):
|
||||
```go
|
||||
// Resource details factory using unified approach
|
||||
resourceConfig := UnifiedResourceConfig{
|
||||
Name: "resource-details", // Must match the href in CustomColumnsOverride
|
||||
ResourceType: "factory",
|
||||
Kind: "ResourceKind", // PascalCase
|
||||
Plural: "resources", // lowercase plural
|
||||
Title: "resource", // lowercase singular
|
||||
}
|
||||
resourceTabs := []any{
|
||||
map[string]any{
|
||||
"key": "details",
|
||||
"label": "Details",
|
||||
"children": []any{
|
||||
contentCard("details-card", map[string]any{
|
||||
"marginBottom": "24px",
|
||||
}, []any{
|
||||
antdText("details-title", true, "Resource details", map[string]any{
|
||||
"fontSize": 20,
|
||||
"marginBottom": "12px",
|
||||
}),
|
||||
spacer("details-spacer", 16),
|
||||
antdRow("details-grid", []any{48, 12}, []any{
|
||||
antdCol("col-left", 12, []any{
|
||||
antdFlexVertical("col-left-stack", 24, []any{
|
||||
// Metadata fields: Name, Namespace, Created, etc.
|
||||
}),
|
||||
}),
|
||||
antdCol("col-right", 12, []any{
|
||||
antdFlexVertical("col-right-stack", 24, []any{
|
||||
// Spec fields
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
resourceSpec := createUnifiedFactory(resourceConfig, resourceTabs, []any{"/api/clusters/{2}/k8s/apis/{group}/{version}/namespaces/{3}/{plural}/{6}"})
|
||||
|
||||
// Add to return list
|
||||
return []*dashboardv1alpha1.Factory{
|
||||
// ... other factories
|
||||
createFactory("resource-details", resourceSpec),
|
||||
}
|
||||
```
|
||||
|
||||
**Key Components**:
|
||||
- **Factory Key**: Must match the href path segment (e.g., `plan-details` matches `/factory/plan-details/...`)
|
||||
- **API Endpoint**: `/api/clusters/{2}/k8s/apis/{group}/{version}/namespaces/{3}/{plural}/{6}`
|
||||
- `{2}`: Cluster name
|
||||
- `{3}`: Namespace
|
||||
- `{6}`: Resource name
|
||||
- **Sidebar Tags**: Automatically set to `["{lowercase-kind}-sidebar"]` by `createUnifiedFactory()`
|
||||
- **Tabs**: Define the detail page content (Details, YAML, etc.)
|
||||
|
||||
**UI Helper Functions**:
|
||||
- `contentCard(id, style, children)`: Container card
|
||||
- `antdText(id, strong, text, style)`: Text element
|
||||
- `antdFlexVertical(id, gap, children)`: Vertical flex container
|
||||
- `antdRow(id, gutter, children)`: Row layout
|
||||
- `antdCol(id, span, children)`: Column layout
|
||||
- `parsedText(id, text, style)`: Text with JSON path parsing
|
||||
- `parsedTextWithFormatter(id, text, formatter)`: Formatted text (e.g., timestamp)
|
||||
- `spacer(id, space)`: Spacing element
|
||||
|
||||
**Displaying Fields**:
|
||||
```go
|
||||
antdFlexVertical("field-block", 4, []any{
|
||||
antdText("field-label", true, "Field Label", nil),
|
||||
parsedText("field-value", "{reqsJsonPath[0]['.spec.fieldName']['-']}", nil),
|
||||
}),
|
||||
```
|
||||
|
||||
### Step 3: Add Sidebar Entry
|
||||
|
||||
The sidebar entry adds navigation to the resource in the sidebar menu.
|
||||
|
||||
**Location**: `internal/controller/dashboard/sidebar.go` in `ensureSidebar()`
|
||||
|
||||
**3a. Add to keysAndTags** (around line 110-116):
|
||||
```go
|
||||
// Add sidebar for {group} {kind} resource
|
||||
keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}
|
||||
```
|
||||
|
||||
**3b. Add Sidebar Section** (if creating a new section, around line 169):
|
||||
```go
|
||||
// Add hardcoded {SectionName} section
|
||||
menuItems = append(menuItems, map[string]any{
|
||||
"key": "{section-key}",
|
||||
"label": "{SectionName}",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"key": "{plural}",
|
||||
"label": "{ResourceLabel}",
|
||||
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/{group}/{version}/{plural}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
**3c. Add Sidebar ID to targetIDs** (around line 220):
|
||||
```go
|
||||
"stock-project-factory-{lowercase-kind}-details",
|
||||
```
|
||||
|
||||
**3d. Update Category Ordering** (if adding a new section):
|
||||
- Update the comment around line 29 to include the new section
|
||||
- Update `orderCategoryLabels()` function if needed
|
||||
- Add skip condition in the category loop (around line 149)
|
||||
|
||||
**Important Notes**:
|
||||
- The sidebar tag (`{lowercase-kind}-sidebar`) must match what the Factory uses
|
||||
- The link format: `/openapi-ui/{clusterName}/{namespace}/api-table/{group}/{version}/{plural}`
|
||||
- All sidebars share the same `keysAndTags` and `menuItems`, so changes affect all sidebar instances
|
||||
|
||||
### Step 4: Verify Integration
|
||||
|
||||
1. **Check Factory-Sidebar Connection**:
|
||||
- Factory uses `sidebarTags: ["{lowercase-kind}-sidebar"]`
|
||||
- Sidebar has `keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}`
|
||||
- Sidebar ID `stock-project-factory-{lowercase-kind}-details` exists in `targetIDs`
|
||||
|
||||
2. **Check Navigation Flow**:
|
||||
- Sidebar link → List table (CustomColumnsOverride)
|
||||
- List table Name column → Detail page (Factory)
|
||||
- All paths use consistent naming
|
||||
|
||||
3. **Test**:
|
||||
- Verify the resource appears in the sidebar
|
||||
- Verify the list table displays correctly
|
||||
- Verify clicking a resource navigates to the detail page
|
||||
- Verify the detail page displays all relevant fields
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Displaying Object References
|
||||
|
||||
For fields that reference other resources (e.g., `applicationRef`, `storageRef`):
|
||||
|
||||
```go
|
||||
// In CustomColumnsOverride
|
||||
createApplicationRefColumn("Application"), // Uses helper function
|
||||
|
||||
// In Factory details tab
|
||||
parsedText("application-ref-value",
|
||||
"{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}",
|
||||
nil),
|
||||
```
|
||||
|
||||
### Displaying Timestamps
|
||||
|
||||
```go
|
||||
antdFlexVertical("created-block", 4, []any{
|
||||
antdText("time-label", true, "Created", nil),
|
||||
antdFlex("time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "time-value",
|
||||
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
```
|
||||
|
||||
### Displaying Namespace with Link
|
||||
|
||||
```go
|
||||
antdFlexVertical("meta-namespace-block", 8, []any{
|
||||
antdText("meta-namespace-label", true, "Namespace", nil),
|
||||
antdFlex("header-row", 6, []any{
|
||||
// Badge component
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "header-badge",
|
||||
"text": "NS",
|
||||
"title": "namespace",
|
||||
"style": map[string]any{
|
||||
"backgroundColor": "#a25792ff",
|
||||
// ... badge styles
|
||||
},
|
||||
},
|
||||
},
|
||||
// Link component
|
||||
map[string]any{
|
||||
"type": "antdLink",
|
||||
"data": map[string]any{
|
||||
"id": "namespace-link",
|
||||
"text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
|
||||
"href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
```
|
||||
|
||||
## File Reference
|
||||
|
||||
- **CustomColumnsOverride**: `internal/controller/dashboard/static_refactored.go` → `CreateAllCustomColumnsOverrides()`
|
||||
- **Factory**: `internal/controller/dashboard/static_refactored.go` → `CreateAllFactories()`
|
||||
- **Sidebar**: `internal/controller/dashboard/sidebar.go` → `ensureSidebar()`
|
||||
- **Helper Functions**: `internal/controller/dashboard/static_helpers.go`
|
||||
- **UI Helpers**: `internal/controller/dashboard/ui_helpers.go`
|
||||
- **Unified Helpers**: `internal/controller/dashboard/unified_helpers.go`
|
||||
|
||||
## AI Agent Prompt Template
|
||||
|
||||
Use this template when asking an AI agent to add a new resource to the dashboard:
|
||||
|
||||
```
|
||||
Please add support for the {ResourceKind} resource ({group}/{version}/{plural}) to the Cozystack dashboard.
|
||||
|
||||
Resource Details:
|
||||
- API Group: {group}
|
||||
- Version: {version}
|
||||
- Kind: {ResourceKind}
|
||||
- Plural: {plural}
|
||||
- Namespaced: {true/false}
|
||||
|
||||
Requirements:
|
||||
1. Add a CustomColumnsOverride in CreateAllCustomColumnsOverrides() with:
|
||||
- ID: stock-namespace-/{group}/{version}/{plural}
|
||||
- Name column with {ResourceKind} badge linking to /factory/{lowercase-kind}-details/{name}
|
||||
- Additional columns: {list relevant columns}
|
||||
|
||||
2. Add a Factory in CreateAllFactories() with:
|
||||
- Key: {lowercase-kind}-details
|
||||
- API endpoint: /api/clusters/{2}/k8s/apis/{group}/{version}/namespaces/{3}/{plural}/{6}
|
||||
- Details tab showing all spec fields:
|
||||
{list spec fields to display}
|
||||
- Use createUnifiedFactory() approach
|
||||
|
||||
3. Add sidebar entry in ensureSidebar():
|
||||
- Add keysAndTags entry: keysAndTags["{plural}"] = []any{"{lowercase-kind}-sidebar"}
|
||||
- Add sidebar section: {specify section name or "add to existing section"}
|
||||
- Add to targetIDs: "stock-project-factory-{lowercase-kind}-details"
|
||||
|
||||
4. Ensure Factory sidebarTags matches the keysAndTags entry
|
||||
|
||||
Please follow the existing patterns in the codebase, particularly the Plan resource implementation as a reference.
|
||||
```
|
||||
|
||||
## Example: Plan Resource
|
||||
|
||||
The Plan resource (`backups.cozystack.io/v1alpha1/plans`) serves as a complete reference implementation:
|
||||
|
||||
- **CustomColumnsOverride**: [Diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-8309b1db3362715b3d94a8b0beae7e95d3ccaf248d4f8702aaa12fba398da895R374-R380) in `static_refactored.go`
|
||||
- **Factory**: [Diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-8309b1db3362715b3d94a8b0beae7e95d3ccaf248d4f8702aaa12fba398da895R1443-R1558) in `static_refactored.go`
|
||||
- **Sidebar**: [Diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-be79027f7179e457a8f10e225bb921a197ffa390eb8f916d8d21379fadd54a56) in `sidebar.go`
|
||||
- **Helper Function**: `createApplicationRefColumn()` in `static_helpers.go` ([diff](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513#diff-f17bcccc089cac3a8e965b13b9ab26e678d45bfc9a58d842399f218703e06a08R1026-R1046))
|
||||
|
||||
Review [this implementation](https://github.com/cozystack/cozystack/compare/1f0b5ff9ac0d9d8896af46f8a19501c8b728671d..88da2d1f642b6cf03873d368dfdc675de23f1513) for a complete working example.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Resource doesn't appear in sidebar
|
||||
- Check that `keysAndTags["{plural}"]` is set correctly
|
||||
- Verify the sidebar section is added to `menuItems`
|
||||
- Ensure the sidebar ID is in `targetIDs`
|
||||
|
||||
### Clicking resource doesn't navigate to detail page
|
||||
- Verify the CustomColumnsOverride href matches the Factory key
|
||||
- Check that the Factory key is exactly `{lowercase-kind}-details`
|
||||
- Ensure the Factory is added to the return list in `CreateAllFactories()`
|
||||
|
||||
### Detail page shows wrong sidebar
|
||||
- Verify Factory `sidebarTags` matches `keysAndTags["{plural}"]`
|
||||
- Check that the sidebar ID `stock-project-factory-{lowercase-kind}-details` exists
|
||||
- Ensure all sidebars are updated (they share the same `keysAndTags`)
|
||||
|
||||
### Fields not displaying correctly
|
||||
- Verify JSON paths are correct (use `.spec.fieldName` format)
|
||||
- Check that `reqsJsonPath[0]` index is used for single resource views
|
||||
- Ensure field names match the actual resource spec structure
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Kubernetes API Conventions: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
|
||||
- Dashboard API Types: `api/dashboard/v1alpha1/`
|
||||
- Resource Types: `api/backups/v1alpha1/` (example)
|
||||
|
||||
@@ -105,8 +105,26 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
|
||||
"properties": map[string]any{},
|
||||
}
|
||||
|
||||
// Check if there's a spec property
|
||||
specProp, ok := props["spec"].(map[string]any)
|
||||
if !ok {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
specProps, ok := specProp["properties"].(map[string]any)
|
||||
if !ok {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
// Create spec.properties structure in schema
|
||||
schemaProps := schema["properties"].(map[string]any)
|
||||
specSchema := map[string]any{
|
||||
"properties": map[string]any{},
|
||||
}
|
||||
schemaProps["spec"] = specSchema
|
||||
|
||||
// Process spec properties recursively
|
||||
processSpecProperties(props, schema["properties"].(map[string]any))
|
||||
processSpecProperties(specProps, specSchema["properties"].(map[string]any))
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
@@ -9,41 +9,46 @@ func TestBuildMultilineStringSchema(t *testing.T) {
|
||||
// Test OpenAPI schema with various field types
|
||||
openAPISchema := `{
|
||||
"properties": {
|
||||
"simpleString": {
|
||||
"type": "string",
|
||||
"description": "A simple string field"
|
||||
},
|
||||
"stringWithEnum": {
|
||||
"type": "string",
|
||||
"enum": ["option1", "option2"],
|
||||
"description": "String with enum should be skipped"
|
||||
},
|
||||
"numberField": {
|
||||
"type": "number",
|
||||
"description": "Number field should be skipped"
|
||||
},
|
||||
"nestedObject": {
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nestedString": {
|
||||
"simpleString": {
|
||||
"type": "string",
|
||||
"description": "Nested string should get multilineString"
|
||||
"description": "A simple string field"
|
||||
},
|
||||
"nestedStringWithEnum": {
|
||||
"stringWithEnum": {
|
||||
"type": "string",
|
||||
"enum": ["a", "b"],
|
||||
"description": "Nested string with enum should be skipped"
|
||||
}
|
||||
}
|
||||
},
|
||||
"arrayOfObjects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"itemString": {
|
||||
"type": "string",
|
||||
"description": "String in array item"
|
||||
"enum": ["option1", "option2"],
|
||||
"description": "String with enum should be skipped"
|
||||
},
|
||||
"numberField": {
|
||||
"type": "number",
|
||||
"description": "Number field should be skipped"
|
||||
},
|
||||
"nestedObject": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nestedString": {
|
||||
"type": "string",
|
||||
"description": "Nested string should get multilineString"
|
||||
},
|
||||
"nestedStringWithEnum": {
|
||||
"type": "string",
|
||||
"enum": ["a", "b"],
|
||||
"description": "Nested string with enum should be skipped"
|
||||
}
|
||||
}
|
||||
},
|
||||
"arrayOfObjects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"itemString": {
|
||||
"type": "string",
|
||||
"description": "String in array item"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,33 +75,44 @@ func TestBuildMultilineStringSchema(t *testing.T) {
|
||||
t.Fatal("schema.properties is not a map")
|
||||
}
|
||||
|
||||
// Check simpleString
|
||||
simpleString, ok := props["simpleString"].(map[string]any)
|
||||
// Check spec property exists
|
||||
spec, ok := props["spec"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("simpleString not found in properties")
|
||||
t.Fatal("spec not found in properties")
|
||||
}
|
||||
|
||||
specProps, ok := spec["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("spec.properties is not a map")
|
||||
}
|
||||
|
||||
// Check simpleString
|
||||
simpleString, ok := specProps["simpleString"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("simpleString not found in spec.properties")
|
||||
}
|
||||
if simpleString["type"] != "multilineString" {
|
||||
t.Errorf("simpleString should have type multilineString, got %v", simpleString["type"])
|
||||
}
|
||||
|
||||
// Check stringWithEnum should not be present (or should not have multilineString)
|
||||
if stringWithEnum, ok := props["stringWithEnum"].(map[string]any); ok {
|
||||
if stringWithEnum, ok := specProps["stringWithEnum"].(map[string]any); ok {
|
||||
if stringWithEnum["type"] == "multilineString" {
|
||||
t.Error("stringWithEnum should not have multilineString type")
|
||||
}
|
||||
}
|
||||
|
||||
// Check numberField should not be present
|
||||
if numberField, ok := props["numberField"].(map[string]any); ok {
|
||||
if numberField, ok := specProps["numberField"].(map[string]any); ok {
|
||||
if numberField["type"] != nil {
|
||||
t.Error("numberField should not have any type override")
|
||||
}
|
||||
}
|
||||
|
||||
// Check nested object
|
||||
nestedObject, ok := props["nestedObject"].(map[string]any)
|
||||
nestedObject, ok := specProps["nestedObject"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("nestedObject not found in properties")
|
||||
t.Fatal("nestedObject not found in spec.properties")
|
||||
}
|
||||
nestedProps, ok := nestedObject["properties"].(map[string]any)
|
||||
if !ok {
|
||||
@@ -113,9 +129,9 @@ func TestBuildMultilineStringSchema(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check array of objects
|
||||
arrayOfObjects, ok := props["arrayOfObjects"].(map[string]any)
|
||||
arrayOfObjects, ok := specProps["arrayOfObjects"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("arrayOfObjects not found in properties")
|
||||
t.Fatal("arrayOfObjects not found in spec.properties")
|
||||
}
|
||||
items, ok := arrayOfObjects["items"].(map[string]any)
|
||||
if !ok {
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
// - Tenant Info (/openapi-ui/{clusterName}/{namespace}/factory/info-details/info)
|
||||
// - All other sections are built from CRDs where spec.dashboard != nil.
|
||||
// - Categories are ordered strictly as:
|
||||
// Marketplace, IaaS, PaaS, NaaS, <others A→Z>, Resources, Administration
|
||||
// Marketplace, IaaS, PaaS, NaaS, <others A→Z>, Resources, Backups, Administration
|
||||
// - Items within each category: sort by Weight (desc), then Label (A→Z).
|
||||
func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
// Build the full menu once.
|
||||
@@ -112,6 +112,15 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
|
||||
keysAndTags["secrets"] = []any{"secret-sidebar"}
|
||||
keysAndTags["ingresses"] = []any{"ingress-sidebar"}
|
||||
|
||||
// Add sidebar for backups.cozystack.io Plan resource
|
||||
keysAndTags["plans"] = []any{"plan-sidebar"}
|
||||
|
||||
// Add sidebar for backups.cozystack.io BackupJob resource
|
||||
keysAndTags["backupjobs"] = []any{"backupjob-sidebar"}
|
||||
|
||||
// Add sidebar for backups.cozystack.io Backup resource
|
||||
keysAndTags["backups"] = []any{"backup-sidebar"}
|
||||
|
||||
// 3) Sort items within each category by Weight (desc), then Label (A→Z)
|
||||
for cat := range categories {
|
||||
sort.Slice(categories[cat], func(i, j int) bool {
|
||||
@@ -123,10 +132,10 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
|
||||
}
|
||||
|
||||
// 4) Order categories strictly:
|
||||
// Marketplace (hardcoded), IaaS, PaaS, NaaS, <others A→Z>, Resources, Administration
|
||||
// Marketplace (hardcoded), IaaS, PaaS, NaaS, <others A→Z>, Resources, Backups (hardcoded), Administration (hardcoded)
|
||||
orderedCats := orderCategoryLabels(categories)
|
||||
|
||||
// 5) Build menuItems (hardcode "Marketplace"; then dynamic categories; then hardcode "Administration")
|
||||
// 5) Build menuItems (hardcode "Marketplace"; then dynamic categories; then hardcode "Backups" and "Administration")
|
||||
menuItems := []any{
|
||||
map[string]any{
|
||||
"key": "marketplace",
|
||||
@@ -142,8 +151,8 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
|
||||
}
|
||||
|
||||
for _, cat := range orderedCats {
|
||||
// Skip "Marketplace" and "Administration" here since they're hardcoded
|
||||
if strings.EqualFold(cat, "Marketplace") || strings.EqualFold(cat, "Administration") {
|
||||
// Skip "Marketplace", "Backups", and "Administration" here since they're hardcoded
|
||||
if strings.EqualFold(cat, "Marketplace") || strings.EqualFold(cat, "Backups") || strings.EqualFold(cat, "Administration") {
|
||||
continue
|
||||
}
|
||||
children := []any{}
|
||||
@@ -163,6 +172,29 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
|
||||
}
|
||||
}
|
||||
|
||||
// Add hardcoded Backups section
|
||||
menuItems = append(menuItems, map[string]any{
|
||||
"key": "backups",
|
||||
"label": "Backups",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"key": "plans",
|
||||
"label": "Plans",
|
||||
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/backups.cozystack.io/v1alpha1/plans",
|
||||
},
|
||||
map[string]any{
|
||||
"key": "backupjobs",
|
||||
"label": "BackupJobs",
|
||||
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/backups.cozystack.io/v1alpha1/backupjobs",
|
||||
},
|
||||
map[string]any{
|
||||
"key": "backups",
|
||||
"label": "Backups",
|
||||
"link": "/openapi-ui/{clusterName}/{namespace}/api-table/backups.cozystack.io/v1alpha1/backups",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Add hardcoded Administration section
|
||||
menuItems = append(menuItems, map[string]any{
|
||||
"key": "administration",
|
||||
@@ -201,6 +233,9 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
|
||||
"stock-project-factory-kube-service-details",
|
||||
"stock-project-factory-kube-secret-details",
|
||||
"stock-project-factory-kube-ingress-details",
|
||||
"stock-project-factory-plan-details",
|
||||
"stock-project-factory-backupjob-details",
|
||||
"stock-project-factory-backup-details",
|
||||
"stock-project-api-form",
|
||||
"stock-project-api-table",
|
||||
"stock-project-builtin-form",
|
||||
|
||||
@@ -1023,6 +1023,27 @@ func createReadyColumn() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// createApplicationRefColumn creates a column that displays
|
||||
// applicationRef in the format "Kind.apiGroup/name"
|
||||
func createApplicationRefColumn(name string) map[string]any {
|
||||
return map[string]any{
|
||||
"name": name,
|
||||
"type": "factory",
|
||||
"customProps": map[string]any{
|
||||
"disableEventBubbling": true,
|
||||
"items": []any{
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"id": "application-ref-text",
|
||||
"text": "{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createConverterBytesColumn creates a column with ConverterBytes component
|
||||
func createConverterBytesColumn(name, jsonPath string) map[string]any {
|
||||
return map[string]any{
|
||||
|
||||
@@ -371,6 +371,30 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid
|
||||
createCustomColumnWithJsonPath("Name", ".metadata.name", "TenantNamespace", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.name']['-']}/factory/marketplace"),
|
||||
createTimestampColumn("Created", ".metadata.creationTimestamp"),
|
||||
}),
|
||||
|
||||
// Stock namespace backups cozystack io v1alpha1 plans
|
||||
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/plans", []any{
|
||||
createCustomColumnWithJsonPath("Name", ".metadata.name", "Plan", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/plan-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
|
||||
createApplicationRefColumn("Application"),
|
||||
createTimestampColumn("Created", ".metadata.creationTimestamp"),
|
||||
}),
|
||||
|
||||
// Stock namespace backups cozystack io v1alpha1 backupjobs
|
||||
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/backupjobs", []any{
|
||||
createCustomColumnWithJsonPath("Name", ".metadata.name", "BackupJob", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/backupjob-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
|
||||
createStringColumn("Phase", ".status.phase"),
|
||||
createApplicationRefColumn("Application"),
|
||||
createTimestampColumn("Created", ".metadata.creationTimestamp"),
|
||||
}),
|
||||
|
||||
// Stock namespace backups cozystack io v1alpha1 backups
|
||||
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/backups", []any{
|
||||
createCustomColumnWithJsonPath("Name", ".metadata.name", "Backup", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/backup-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
|
||||
createStringColumn("Phase", ".status.phase"),
|
||||
createApplicationRefColumn("Application"),
|
||||
createTimestampColumn("Taken At", ".spec.takenAt"),
|
||||
createTimestampColumn("Created", ".metadata.creationTimestamp"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1433,6 +1457,434 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
|
||||
}
|
||||
workloadmonitorSpec := createFactorySpec("workloadmonitor-details", []any{"workloadmonitor-sidebar"}, []any{"/api/clusters/{2}/k8s/apis/cozystack.io/v1alpha1/namespaces/{3}/workloadmonitors/{6}"}, workloadmonitorHeader, workloadmonitorTabs)
|
||||
|
||||
// Plan details factory using unified approach
|
||||
planConfig := UnifiedResourceConfig{
|
||||
Name: "plan-details",
|
||||
ResourceType: "factory",
|
||||
Kind: "Plan",
|
||||
Plural: "plans",
|
||||
Title: "plan",
|
||||
}
|
||||
planTabs := []any{
|
||||
map[string]any{
|
||||
"key": "details",
|
||||
"label": "Details",
|
||||
"children": []any{
|
||||
contentCard("details-card", map[string]any{
|
||||
"marginBottom": "24px",
|
||||
}, []any{
|
||||
antdText("details-title", true, "Plan details", map[string]any{
|
||||
"fontSize": 20,
|
||||
"marginBottom": "12px",
|
||||
}),
|
||||
spacer("details-spacer", 16),
|
||||
antdRow("details-grid", []any{48, 12}, []any{
|
||||
antdCol("col-left", 12, []any{
|
||||
antdFlexVertical("col-left-stack", 24, []any{
|
||||
antdFlexVertical("meta-name-block", 4, []any{
|
||||
antdText("meta-name-label", true, "Name", nil),
|
||||
parsedText("meta-name-value", "{reqsJsonPath[0]['.metadata.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("meta-namespace-block", 8, []any{
|
||||
antdText("meta-namespace-label", true, "Namespace", nil),
|
||||
antdFlex("header-row", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "header-badge",
|
||||
"text": "NS",
|
||||
"title": "namespace",
|
||||
"style": map[string]any{
|
||||
"backgroundColor": "#a25792ff",
|
||||
"borderRadius": "20px",
|
||||
"color": "#fff",
|
||||
"display": "inline-block",
|
||||
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
|
||||
"fontSize": "15px",
|
||||
"fontWeight": 400,
|
||||
"lineHeight": "24px",
|
||||
"minWidth": 24,
|
||||
"padding": "0 9px",
|
||||
"textAlign": "center",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "antdLink",
|
||||
"data": map[string]any{
|
||||
"id": "namespace-link",
|
||||
"text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
|
||||
"href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("meta-created-block", 4, []any{
|
||||
antdText("time-label", true, "Created", nil),
|
||||
antdFlex("time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "time-value",
|
||||
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
antdCol("col-right", 12, []any{
|
||||
antdFlexVertical("col-right-stack", 24, []any{
|
||||
antdFlexVertical("spec-application-ref-block", 4, []any{
|
||||
antdText("application-ref-label", true, "Application", nil),
|
||||
parsedText("application-ref-value", "{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-storage-ref-block", 4, []any{
|
||||
antdText("storage-ref-label", true, "Storage", nil),
|
||||
parsedText("storage-ref-value", "{reqsJsonPath[0]['.spec.storageRef.kind']['-']}.{reqsJsonPath[0]['.spec.storageRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.storageRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-strategy-ref-block", 4, []any{
|
||||
antdText("strategy-ref-label", true, "Strategy", nil),
|
||||
parsedText("strategy-ref-value", "{reqsJsonPath[0]['.spec.strategyRef.kind']['-']}.{reqsJsonPath[0]['.spec.strategyRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.strategyRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-schedule-type-block", 4, []any{
|
||||
antdText("schedule-type-label", true, "Schedule Type", nil),
|
||||
parsedText("schedule-type-value", "{reqsJsonPath[0]['.spec.schedule.type']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-schedule-cron-block", 4, []any{
|
||||
antdText("schedule-cron-label", true, "Schedule Cron", nil),
|
||||
parsedText("schedule-cron-value", "{reqsJsonPath[0]['.spec.schedule.cron']['-']}", nil),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
planSpec := createUnifiedFactory(planConfig, planTabs, []any{"/api/clusters/{2}/k8s/apis/backups.cozystack.io/v1alpha1/namespaces/{3}/plans/{6}"})
|
||||
|
||||
// BackupJob details factory using unified approach
|
||||
backupJobConfig := UnifiedResourceConfig{
|
||||
Name: "backupjob-details",
|
||||
ResourceType: "factory",
|
||||
Kind: "BackupJob",
|
||||
Plural: "backupjobs",
|
||||
Title: "backupjob",
|
||||
}
|
||||
backupJobTabs := []any{
|
||||
map[string]any{
|
||||
"key": "details",
|
||||
"label": "Details",
|
||||
"children": []any{
|
||||
contentCard("details-card", map[string]any{
|
||||
"marginBottom": "24px",
|
||||
}, []any{
|
||||
antdText("details-title", true, "BackupJob details", map[string]any{
|
||||
"fontSize": 20,
|
||||
"marginBottom": "12px",
|
||||
}),
|
||||
spacer("details-spacer", 16),
|
||||
antdRow("details-grid", []any{48, 12}, []any{
|
||||
antdCol("col-left", 12, []any{
|
||||
antdFlexVertical("col-left-stack", 24, []any{
|
||||
antdFlexVertical("meta-name-block", 4, []any{
|
||||
antdText("meta-name-label", true, "Name", nil),
|
||||
parsedText("meta-name-value", "{reqsJsonPath[0]['.metadata.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("meta-namespace-block", 8, []any{
|
||||
antdText("meta-namespace-label", true, "Namespace", nil),
|
||||
antdFlex("header-row", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "header-badge",
|
||||
"text": "NS",
|
||||
"title": "namespace",
|
||||
"style": map[string]any{
|
||||
"backgroundColor": "#a25792ff",
|
||||
"borderRadius": "20px",
|
||||
"color": "#fff",
|
||||
"display": "inline-block",
|
||||
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
|
||||
"fontSize": "15px",
|
||||
"fontWeight": 400,
|
||||
"lineHeight": "24px",
|
||||
"minWidth": 24,
|
||||
"padding": "0 9px",
|
||||
"textAlign": "center",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "antdLink",
|
||||
"data": map[string]any{
|
||||
"id": "namespace-link",
|
||||
"text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
|
||||
"href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("meta-created-block", 4, []any{
|
||||
antdText("time-label", true, "Created", nil),
|
||||
antdFlex("time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "time-value",
|
||||
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
antdCol("col-right", 12, []any{
|
||||
antdFlexVertical("col-right-stack", 24, []any{
|
||||
antdFlexVertical("status-phase-block", 4, []any{
|
||||
antdText("phase-label", true, "Phase", nil),
|
||||
parsedText("phase-value", "{reqsJsonPath[0]['.status.phase']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-plan-ref-block", 4, []any{
|
||||
antdText("plan-ref-label", true, "Plan Ref", nil),
|
||||
parsedText("plan-ref-value", "{reqsJsonPath[0]['.spec.planRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-application-ref-block", 4, []any{
|
||||
antdText("application-ref-label", true, "Application", nil),
|
||||
parsedText("application-ref-value", "{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-storage-ref-block", 4, []any{
|
||||
antdText("storage-ref-label", true, "Storage", nil),
|
||||
parsedText("storage-ref-value", "{reqsJsonPath[0]['.spec.storageRef.kind']['-']}.{reqsJsonPath[0]['.spec.storageRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.storageRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-strategy-ref-block", 4, []any{
|
||||
antdText("strategy-ref-label", true, "Strategy", nil),
|
||||
parsedText("strategy-ref-value", "{reqsJsonPath[0]['.spec.strategyRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("status-backup-ref-block", 4, []any{
|
||||
antdText("backup-ref-label", true, "Backup Ref", nil),
|
||||
parsedText("backup-ref-value", "{reqsJsonPath[0]['.status.backupRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("status-started-at-block", 4, []any{
|
||||
antdText("started-at-label", true, "Started At", nil),
|
||||
antdFlex("time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "time-value",
|
||||
"text": "{reqsJsonPath[0]['.status.startedAt']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("status-completed-at-block", 4, []any{
|
||||
antdText("completed-at-label", true, "Completed At", nil),
|
||||
antdFlex("time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "time-value",
|
||||
"text": "{reqsJsonPath[0]['.status.completedAt']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("status-message-block", 4, []any{
|
||||
antdText("message-label", true, "Message", nil),
|
||||
parsedText("message-value", "{reqsJsonPath[0]['.status.message']['-']}", nil),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
backupJobSpec := createUnifiedFactory(backupJobConfig, backupJobTabs, []any{"/api/clusters/{2}/k8s/apis/backups.cozystack.io/v1alpha1/namespaces/{3}/backupjobs/{6}"})
|
||||
|
||||
// Backup details factory using unified approach
|
||||
backupConfig := UnifiedResourceConfig{
|
||||
Name: "backup-details",
|
||||
ResourceType: "factory",
|
||||
Kind: "Backup",
|
||||
Plural: "backups",
|
||||
Title: "backup",
|
||||
}
|
||||
backupTabs := []any{
|
||||
map[string]any{
|
||||
"key": "details",
|
||||
"label": "Details",
|
||||
"children": []any{
|
||||
contentCard("details-card", map[string]any{
|
||||
"marginBottom": "24px",
|
||||
}, []any{
|
||||
antdText("details-title", true, "Backup details", map[string]any{
|
||||
"fontSize": 20,
|
||||
"marginBottom": "12px",
|
||||
}),
|
||||
spacer("details-spacer", 16),
|
||||
antdRow("details-grid", []any{48, 12}, []any{
|
||||
antdCol("col-left", 12, []any{
|
||||
antdFlexVertical("col-left-stack", 24, []any{
|
||||
antdFlexVertical("meta-name-block", 4, []any{
|
||||
antdText("meta-name-label", true, "Name", nil),
|
||||
parsedText("meta-name-value", "{reqsJsonPath[0]['.metadata.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("meta-namespace-block", 8, []any{
|
||||
antdText("meta-namespace-label", true, "Namespace", nil),
|
||||
antdFlex("header-row", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "header-badge",
|
||||
"text": "NS",
|
||||
"title": "namespace",
|
||||
"style": map[string]any{
|
||||
"backgroundColor": "#a25792ff",
|
||||
"borderRadius": "20px",
|
||||
"color": "#fff",
|
||||
"display": "inline-block",
|
||||
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
|
||||
"fontSize": "15px",
|
||||
"fontWeight": 400,
|
||||
"lineHeight": "24px",
|
||||
"minWidth": 24,
|
||||
"padding": "0 9px",
|
||||
"textAlign": "center",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "antdLink",
|
||||
"data": map[string]any{
|
||||
"id": "namespace-link",
|
||||
"text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
|
||||
"href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("meta-created-block", 4, []any{
|
||||
antdText("time-label", true, "Created", nil),
|
||||
antdFlex("time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "time-value",
|
||||
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
antdCol("col-right", 12, []any{
|
||||
antdFlexVertical("col-right-stack", 24, []any{
|
||||
antdFlexVertical("status-phase-block", 4, []any{
|
||||
antdText("phase-label", true, "Phase", nil),
|
||||
parsedText("phase-value", "{reqsJsonPath[0]['.status.phase']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-taken-at-block", 4, []any{
|
||||
antdText("taken-at-label", true, "Taken At", nil),
|
||||
antdFlex("time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "time-value",
|
||||
"text": "{reqsJsonPath[0]['.spec.takenAt']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("spec-plan-ref-block", 4, []any{
|
||||
antdText("plan-ref-label", true, "Plan Ref", nil),
|
||||
parsedText("plan-ref-value", "{reqsJsonPath[0]['.spec.planRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-application-ref-block", 4, []any{
|
||||
antdText("application-ref-label", true, "Application", nil),
|
||||
parsedText("application-ref-value", "{reqsJsonPath[0]['.spec.applicationRef.kind']['-']}.{reqsJsonPath[0]['.spec.applicationRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.applicationRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-storage-ref-block", 4, []any{
|
||||
antdText("storage-ref-label", true, "Storage", nil),
|
||||
parsedText("storage-ref-value", "{reqsJsonPath[0]['.spec.storageRef.kind']['-']}.{reqsJsonPath[0]['.spec.storageRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.storageRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-strategy-ref-block", 4, []any{
|
||||
antdText("strategy-ref-label", true, "Strategy", nil),
|
||||
parsedText("strategy-ref-value", "{reqsJsonPath[0]['.spec.strategyRef.kind']['-']}.{reqsJsonPath[0]['.spec.strategyRef.apiGroup']['-']}/{reqsJsonPath[0]['.spec.strategyRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("status-artifact-uri-block", 4, []any{
|
||||
antdText("artifact-uri-label", true, "Artifact URI", nil),
|
||||
parsedText("artifact-uri-value", "{reqsJsonPath[0]['.status.artifact.uri']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("status-artifact-size-block", 4, []any{
|
||||
antdText("artifact-size-label", true, "Artifact Size", nil),
|
||||
parsedText("artifact-size-value", "{reqsJsonPath[0]['.status.artifact.sizeBytes']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("status-artifact-checksum-block", 4, []any{
|
||||
antdText("artifact-checksum-label", true, "Artifact Checksum", nil),
|
||||
parsedText("artifact-checksum-value", "{reqsJsonPath[0]['.status.artifact.checksum']['-']}", nil),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
backupSpec := createUnifiedFactory(backupConfig, backupTabs, []any{"/api/clusters/{2}/k8s/apis/backups.cozystack.io/v1alpha1/namespaces/{3}/backups/{6}"})
|
||||
|
||||
return []*dashboardv1alpha1.Factory{
|
||||
createFactory("marketplace", marketplaceSpec),
|
||||
createFactory("namespace-details", namespaceSpec),
|
||||
@@ -1442,6 +1894,9 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
|
||||
createFactory("kube-service-details", serviceSpec),
|
||||
createFactory("kube-ingress-details", ingressSpec),
|
||||
createFactory("workloadmonitor-details", workloadmonitorSpec),
|
||||
createFactory("plan-details", planSpec),
|
||||
createFactory("backupjob-details", backupJobSpec),
|
||||
createFactory("backup-details", backupSpec),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
68
internal/template/template.go
Normal file
68
internal/template/template.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
tmpl "text/template"
|
||||
)
|
||||
|
||||
func Template[T any](obj *T, templateContext map[string]any) (*T, error) {
|
||||
b, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var unstructured any
|
||||
err = json.Unmarshal(b, &unstructured)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templateFunc := func(in string) string {
|
||||
out, err := template(in, templateContext)
|
||||
if err != nil {
|
||||
return in
|
||||
}
|
||||
return out
|
||||
}
|
||||
unstructured = mapAtStrings(unstructured, templateFunc)
|
||||
b, err = json.Marshal(unstructured)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out T
|
||||
err = json.Unmarshal(b, &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func mapAtStrings(v any, f func(string) string) any {
|
||||
switch x := v.(type) {
|
||||
case map[string]any:
|
||||
for k, val := range x {
|
||||
x[k] = mapAtStrings(val, f)
|
||||
}
|
||||
return x
|
||||
case []any:
|
||||
for i, val := range x {
|
||||
x[i] = mapAtStrings(val, f)
|
||||
}
|
||||
return x
|
||||
case string:
|
||||
return f(x)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func template(in string, templateContext map[string]any) (string, error) {
|
||||
tpl, err := tmpl.New("this").Parse(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, templateContext); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
68
internal/template/template_test.go
Normal file
68
internal/template/template_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestTemplate_PodTemplateSpec(t *testing.T) {
|
||||
original := corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod",
|
||||
Labels: map[string]string{
|
||||
"app": "demo",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"note": "hello",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "{{ .Release.Name }}",
|
||||
Image: "nginx:1.21",
|
||||
Args: []string{"--flag={{ .Values.value }}"},
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: "FOO",
|
||||
Value: "{{ .Release.Namespace }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
templateContext := map[string]any{
|
||||
"Release": map[string]any{
|
||||
"Name": "foo",
|
||||
"Namespace": "notdefault",
|
||||
},
|
||||
"Values": map[string]any{
|
||||
"value": 3,
|
||||
},
|
||||
}
|
||||
reference := *original.DeepCopy()
|
||||
reference.Spec.Containers[0].Name = "foo"
|
||||
reference.Spec.Containers[0].Args[0] = "--flag=3"
|
||||
reference.Spec.Containers[0].Env[0].Value = "notdefault"
|
||||
got, err := Template(&original, templateContext)
|
||||
if err != nil {
|
||||
t.Fatalf("Template returned error: %v", err)
|
||||
}
|
||||
b1, err := json.Marshal(reference)
|
||||
t.Logf("reference:\n%s", string(b1))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal reference value: %v", err)
|
||||
}
|
||||
b2, err := json.Marshal(got)
|
||||
t.Logf("got:\n%s", string(b2))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal transformed value: %v", err)
|
||||
}
|
||||
if string(b1) != string(b2) {
|
||||
t.Fatalf("transformed value not equal to reference value, expected: %s, got: %s", string(b1), string(b2))
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:e0a07082bb6fc6aeaae2315f335386f1705a646c72f9e0af512aebbca5cb2b15
|
||||
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:b7633717cd7449c0042ae92d8ca9b36e4d69566561f5c7d44e21058e7d05c6d5
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:2d39989846c3579dd020b9f6c77e6e314cc81aa344eaac0f6d633e723c17196d
|
||||
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:e1c0eb54d0ddadc8c7d72cee4f4282d56c957bf4ff0f62ef37db6b38dd0e8a01
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/pkg/controller/kubevirteps/kubevirteps_controller.go b/pkg/controller/kubevirteps/kubevirteps_controller.go
|
||||
index 53388eb8e..28644236f 100644
|
||||
index 53388eb8e..873060251 100644
|
||||
--- a/pkg/controller/kubevirteps/kubevirteps_controller.go
|
||||
+++ b/pkg/controller/kubevirteps/kubevirteps_controller.go
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
@@ -10,12 +10,17 @@ index 53388eb8e..28644236f 100644
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
@@ -669,35 +668,50 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
@@ -666,38 +665,62 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
// for extracting the nodes it does not matter what type of address we are dealing with
|
||||
// all nodes with an endpoint for a corresponding slice will be selected.
|
||||
nodeSet := sets.Set[string]{}
|
||||
+ hasEndpointsWithoutNodeName := false
|
||||
for _, slice := range tenantSlices {
|
||||
for _, endpoint := range slice.Endpoints {
|
||||
// find all unique nodes that correspond to an endpoint in a tenant slice
|
||||
+ if endpoint.NodeName == nil {
|
||||
+ klog.Warningf("Skipping endpoint without NodeName in slice %s/%s", slice.Namespace, slice.Name)
|
||||
+ hasEndpointsWithoutNodeName = true
|
||||
+ continue
|
||||
+ }
|
||||
nodeSet.Insert(*endpoint.NodeName)
|
||||
@@ -23,6 +28,13 @@ index 53388eb8e..28644236f 100644
|
||||
}
|
||||
|
||||
- klog.Infof("Desired nodes for service %s in namespace %s: %v", service.Name, service.Namespace, sets.List(nodeSet))
|
||||
+ // Fallback: if no endpoints with NodeName were found, but there are endpoints without NodeName,
|
||||
+ // distribute traffic to all VMIs (similar to ExternalTrafficPolicy=Cluster behavior)
|
||||
+ if nodeSet.Len() == 0 && hasEndpointsWithoutNodeName {
|
||||
+ klog.Infof("No endpoints with NodeName found for service %s/%s, falling back to all VMIs", service.Namespace, service.Name)
|
||||
+ return c.getAllVMIEndpoints()
|
||||
+ }
|
||||
+
|
||||
+ klog.Infof("Desired nodes for service %s/%s: %v", service.Namespace, service.Name, sets.List(nodeSet))
|
||||
|
||||
for _, node := range sets.List(nodeSet) {
|
||||
@@ -68,7 +80,7 @@ index 53388eb8e..28644236f 100644
|
||||
desiredEndpoints = append(desiredEndpoints, &discovery.Endpoint{
|
||||
Addresses: []string{i.IP},
|
||||
Conditions: discovery.EndpointConditions{
|
||||
@@ -705,9 +719,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
@@ -705,9 +728,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
Serving: &serving,
|
||||
Terminating: &terminating,
|
||||
},
|
||||
@@ -80,6 +92,71 @@ index 53388eb8e..28644236f 100644
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -716,6 +739,64 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
return desiredEndpoints
|
||||
}
|
||||
|
||||
+// getAllVMIEndpoints returns endpoints for all VMIs in the infra namespace.
|
||||
+// This is used as a fallback when tenant endpoints don't have NodeName specified,
|
||||
+// similar to ExternalTrafficPolicy=Cluster behavior where traffic is distributed to all nodes.
|
||||
+func (c *Controller) getAllVMIEndpoints() []*discovery.Endpoint {
|
||||
+ var endpoints []*discovery.Endpoint
|
||||
+
|
||||
+ // List all VMIs in the infra namespace
|
||||
+ vmiList, err := c.infraDynamic.
|
||||
+ Resource(kubevirtv1.VirtualMachineInstanceGroupVersionKind.GroupVersion().WithResource("virtualmachineinstances")).
|
||||
+ Namespace(c.infraNamespace).
|
||||
+ List(context.TODO(), metav1.ListOptions{})
|
||||
+ if err != nil {
|
||||
+ klog.Errorf("Failed to list VMIs in namespace %q: %v", c.infraNamespace, err)
|
||||
+ return endpoints
|
||||
+ }
|
||||
+
|
||||
+ for _, obj := range vmiList.Items {
|
||||
+ vmi := &kubevirtv1.VirtualMachineInstance{}
|
||||
+ err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, vmi)
|
||||
+ if err != nil {
|
||||
+ klog.Errorf("Failed to convert Unstructured to VirtualMachineInstance: %v", err)
|
||||
+ continue
|
||||
+ }
|
||||
+
|
||||
+ if vmi.Status.NodeName == "" {
|
||||
+ klog.Warningf("Skipping VMI %s/%s: NodeName is empty", vmi.Namespace, vmi.Name)
|
||||
+ continue
|
||||
+ }
|
||||
+ nodeNamePtr := &vmi.Status.NodeName
|
||||
+
|
||||
+ ready := vmi.Status.Phase == kubevirtv1.Running
|
||||
+ serving := vmi.Status.Phase == kubevirtv1.Running
|
||||
+ terminating := vmi.Status.Phase == kubevirtv1.Failed || vmi.Status.Phase == kubevirtv1.Succeeded
|
||||
+
|
||||
+ for _, i := range vmi.Status.Interfaces {
|
||||
+ if i.Name == "default" {
|
||||
+ if i.IP == "" {
|
||||
+ klog.Warningf("VMI %s/%s interface %q has no IP, skipping", vmi.Namespace, vmi.Name, i.Name)
|
||||
+ continue
|
||||
+ }
|
||||
+ endpoints = append(endpoints, &discovery.Endpoint{
|
||||
+ Addresses: []string{i.IP},
|
||||
+ Conditions: discovery.EndpointConditions{
|
||||
+ Ready: &ready,
|
||||
+ Serving: &serving,
|
||||
+ Terminating: &terminating,
|
||||
+ },
|
||||
+ NodeName: nodeNamePtr,
|
||||
+ })
|
||||
+ break
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ klog.Infof("Fallback: created %d endpoints from all VMIs in namespace %s", len(endpoints), c.infraNamespace)
|
||||
+ return endpoints
|
||||
+}
|
||||
+
|
||||
func (c *Controller) ensureEndpointSliceLabels(slice *discovery.EndpointSlice, svc *v1.Service) (map[string]string, bool) {
|
||||
labels := make(map[string]string)
|
||||
labelsChanged := false
|
||||
diff --git a/pkg/controller/kubevirteps/kubevirteps_controller_test.go b/pkg/controller/kubevirteps/kubevirteps_controller_test.go
|
||||
index 1c97035b4..d205d0bed 100644
|
||||
--- a/pkg/controller/kubevirteps/kubevirteps_controller_test.go
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:d5c836ba33cf5dbed7e6f866784f668f80ffe69179e7c75847b680111984eefb
|
||||
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:ae0c5e3e005388d865617097a9fe4b4e8a2be6b62cec893e08f2143d0acde241
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "tenant.name" . }}-cleanup
|
||||
namespace: {{ include "tenant.name" . }}
|
||||
namespace: cozy-system
|
||||
annotations:
|
||||
helm.sh/hook: pre-delete
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
|
||||
@@ -39,13 +39,13 @@ roleRef:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "tenant.name" . }}-cleanup
|
||||
namespace: {{ include "tenant.name" . }}
|
||||
namespace: cozy-system
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "tenant.name" . }}-cleanup
|
||||
namespace: {{ include "tenant.name" . }}
|
||||
namespace: cozy-system
|
||||
annotations:
|
||||
helm.sh/hook: pre-delete
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
|
||||
|
||||
@@ -67,6 +67,19 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if ne (include "tenant.name" .) "tenant-root" }}
|
||||
- toEndpoints:
|
||||
{{- if hasPrefix "tenant-" .Release.Namespace }}
|
||||
{{- $parts := splitList "-" .Release.Namespace }}
|
||||
{{- range $i, $v := $parts }}
|
||||
{{- if ne $i 0 }}
|
||||
- matchLabels:
|
||||
cozystack.io/service: ingress
|
||||
"k8s:io.kubernetes.pod.namespace": {{ join "-" (slice $parts 0 (add $i 1)) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: cilium.io/v2
|
||||
kind: CiliumClusterwideNetworkPolicy
|
||||
|
||||
@@ -69,3 +69,36 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
|
||||
{{- end }}
|
||||
{{- $uuid }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Node Affinity for Windows VMs
|
||||
*/}}
|
||||
{{- define "virtual-machine.nodeAffinity" -}}
|
||||
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" -}}
|
||||
{{- if $configMap -}}
|
||||
{{- $dedicatedNodesForWindowsVMs := get $configMap.data "dedicatedNodesForWindowsVMs" -}}
|
||||
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
|
||||
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
{{- if $isWindows }}
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: In
|
||||
values:
|
||||
- "true"
|
||||
{{- else }}
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: NotIn
|
||||
values:
|
||||
- "true"
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{{- if .Values.external }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -7,17 +6,24 @@ metadata:
|
||||
labels:
|
||||
apps.cozystack.io/user-service: "true"
|
||||
{{- include "virtual-machine.labels" . | nindent 4 }}
|
||||
{{- if .Values.external }}
|
||||
annotations:
|
||||
networking.cozystack.io/wholeIP: "true"
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
|
||||
{{- if .Values.external }}
|
||||
externalTrafficPolicy: Local
|
||||
{{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }}
|
||||
allocateLoadBalancerNodePorts: false
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
clusterIP: None
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "virtual-machine.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
{{- if .Values.external }}
|
||||
{{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }}
|
||||
- port: 65535
|
||||
{{- else }}
|
||||
@@ -27,4 +33,6 @@ spec:
|
||||
targetPort: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- port: 65535
|
||||
{{- end }}
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
{{- if and $existingPVC $desiredStorage -}}
|
||||
{{- $currentStorage := $existingPVC.spec.resources.requests.storage | toString -}}
|
||||
{{- if not (eq $currentStorage $desiredStorage) -}}
|
||||
{{- $needResizePVC = true -}}
|
||||
{{- $oldSize := (include "cozy-lib.resources.toFloat" $currentStorage) | float64 -}}
|
||||
{{- $newSize := (include "cozy-lib.resources.toFloat" $desiredStorage) | float64 -}}
|
||||
{{- if gt $newSize $oldSize -}}
|
||||
{{- $needResizePVC = true -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
@@ -124,6 +124,8 @@ spec:
|
||||
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
{{- include "virtual-machine.nodeAffinity" . | nindent 6 }}
|
||||
|
||||
volumes:
|
||||
- name: systemdisk
|
||||
dataVolume:
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
{{- $existingPVC := lookup "v1" "PersistentVolumeClaim" .Release.Namespace .Release.Name }}
|
||||
{{- if and $existingPVC (ne ($existingPVC.spec.resources.requests.storage | toString) .Values.storage) -}}
|
||||
{{- $shouldResize := false -}}
|
||||
{{- if and $existingPVC .Values.storage -}}
|
||||
{{- $currentStorage := $existingPVC.spec.resources.requests.storage | toString -}}
|
||||
{{- if ne $currentStorage .Values.storage -}}
|
||||
{{- $oldSize := (include "cozy-lib.resources.toFloat" $currentStorage) | float64 -}}
|
||||
{{- $newSize := (include "cozy-lib.resources.toFloat" .Values.storage) | float64 -}}
|
||||
{{- if gt $newSize $oldSize -}}
|
||||
{{- $shouldResize = true -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if $shouldResize -}}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
@@ -23,6 +35,7 @@ spec:
|
||||
command: ["sh", "-xec"]
|
||||
args:
|
||||
- |
|
||||
echo "Resizing PVC to {{ .Values.storage }}..."
|
||||
kubectl patch pvc {{ .Release.Name }} -p '{"spec":{"resources":{"requests":{"storage":"{{ .Values.storage }}"}}}}'
|
||||
---
|
||||
apiVersion: v1
|
||||
|
||||
@@ -69,3 +69,36 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
|
||||
{{- end }}
|
||||
{{- $uuid }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Node Affinity for Windows VMs
|
||||
*/}}
|
||||
{{- define "virtual-machine.nodeAffinity" -}}
|
||||
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" -}}
|
||||
{{- if $configMap -}}
|
||||
{{- $dedicatedNodesForWindowsVMs := get $configMap.data "dedicatedNodesForWindowsVMs" -}}
|
||||
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
|
||||
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
{{- if $isWindows }}
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: In
|
||||
values:
|
||||
- "true"
|
||||
{{- else }}
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: NotIn
|
||||
values:
|
||||
- "true"
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{{- if .Values.external }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -7,17 +6,24 @@ metadata:
|
||||
labels:
|
||||
apps.cozystack.io/user-service: "true"
|
||||
{{- include "virtual-machine.labels" . | nindent 4 }}
|
||||
{{- if .Values.external }}
|
||||
annotations:
|
||||
networking.cozystack.io/wholeIP: "true"
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
|
||||
{{- if .Values.external }}
|
||||
externalTrafficPolicy: Local
|
||||
{{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }}
|
||||
allocateLoadBalancerNodePorts: false
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
clusterIP: None
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "virtual-machine.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
{{- if .Values.external }}
|
||||
{{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }}
|
||||
- port: 65535
|
||||
{{- else }}
|
||||
@@ -27,4 +33,6 @@ spec:
|
||||
targetPort: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- port: 65535
|
||||
{{- end }}
|
||||
|
||||
@@ -95,6 +95,9 @@ spec:
|
||||
noCloud: {}
|
||||
{{- end }}
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
{{- include "virtual-machine.nodeAffinity" . | nindent 6 }}
|
||||
|
||||
volumes:
|
||||
{{- range .Values.disks }}
|
||||
- name: disk-{{ .name }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24-alpine as k8s-await-election-builder
|
||||
FROM golang:1.24-alpine AS k8s-await-election-builder
|
||||
|
||||
ARG K8S_AWAIT_ELECTION_GITREPO=https://github.com/LINBIT/k8s-await-election
|
||||
ARG K8S_AWAIT_ELECTION_VERSION=0.4.1
|
||||
@@ -13,7 +13,7 @@ RUN git clone ${K8S_AWAIT_ELECTION_GITREPO} /usr/local/go/k8s-await-election/ \
|
||||
&& make \
|
||||
&& mv ./out/k8s-await-election-${TARGETARCH} /k8s-await-election
|
||||
|
||||
FROM golang:1.24-alpine as builder
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
cozystack:
|
||||
image: ghcr.io/cozystack/cozystack/installer:v0.38.2@sha256:9ff92b655de6f9bea3cba4cd42dcffabd9aace6966dcfb1cc02dda2420ea4a15
|
||||
image: ghcr.io/cozystack/cozystack/installer:v0.39.2@sha256:1c0eef43e1a06f5bf653c0bb7a2821a623fcad71c41c2c15b080fe0f6c809e39
|
||||
|
||||
@@ -112,6 +112,12 @@ releases:
|
||||
disableTelemetry: true
|
||||
{{- end }}
|
||||
|
||||
- name: backup-controller
|
||||
releaseName: backup-controller
|
||||
chart: cozy-backup-controller
|
||||
namespace: cozy-backup-controller
|
||||
dependsOn: [cilium,kubeovn]
|
||||
|
||||
- name: lineage-controller-webhook
|
||||
releaseName: lineage-controller-webhook
|
||||
chart: cozy-lineage-controller-webhook
|
||||
|
||||
@@ -56,6 +56,11 @@ releases:
|
||||
disableTelemetry: true
|
||||
{{- end }}
|
||||
|
||||
- name: backup-controller
|
||||
releaseName: backup-controller
|
||||
chart: cozy-backup-controller
|
||||
namespace: cozy-backup-controller
|
||||
|
||||
- name: lineage-controller-webhook
|
||||
releaseName: lineage-controller-webhook
|
||||
chart: cozy-lineage-controller-webhook
|
||||
|
||||
0
packages/core/testing/Chart.yaml
Executable file → Normal file
0
packages/core/testing/Chart.yaml
Executable file → Normal file
0
packages/core/testing/Makefile
Executable file → Normal file
0
packages/core/testing/Makefile
Executable file → Normal file
10
packages/core/testing/images/e2e-sandbox/Dockerfile
Executable file → Normal file
10
packages/core/testing/images/e2e-sandbox/Dockerfile
Executable file → Normal file
@@ -9,7 +9,7 @@ ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apt update -q
|
||||
RUN apt install -yq --no-install-recommends psmisc genisoimage ca-certificates qemu-kvm qemu-utils iproute2 iptables wget xz-utils netcat curl jq make git
|
||||
RUN apt install -yq --no-install-recommends psmisc genisoimage ca-certificates qemu-kvm qemu-utils iproute2 iptables wget xz-utils netcat curl jq make git bash-completion
|
||||
RUN curl -sSL "https://github.com/siderolabs/talos/releases/download/v${TALOSCTL_VERSION}/talosctl-${TARGETOS}-${TARGETARCH}" -o /usr/local/bin/talosctl \
|
||||
&& chmod +x /usr/local/bin/talosctl
|
||||
RUN curl -sSL "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/${TARGETOS}/${TARGETARCH}/kubectl" -o /usr/local/bin/kubectl \
|
||||
@@ -21,5 +21,13 @@ RUN curl -sSL "https://fluxcd.io/install.sh" | bash
|
||||
RUN curl -sSL "https://github.com/cozystack/cozypkg/raw/refs/heads/main/hack/install.sh" | sh -s -- -v "${COZYPKG_VERSION}"
|
||||
RUN curl https://dl.min.io/client/mc/release/${TARGETOS}-${TARGETARCH}/mc --create-dirs -o /usr/local/bin/mc \
|
||||
&& chmod +x /usr/local/bin/mc
|
||||
RUN <<'EOF'
|
||||
cat <<'EOT' >> /etc/bash.bashrc
|
||||
. /etc/bash_completion
|
||||
. <(kubectl completion bash)
|
||||
alias k=kubectl
|
||||
complete -F __start_kubectl k
|
||||
EOT
|
||||
EOF
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
2
packages/core/testing/values.yaml
Executable file → Normal file
2
packages/core/testing/values.yaml
Executable file → Normal file
@@ -1,2 +1,2 @@
|
||||
e2e:
|
||||
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v0.38.2@sha256:84be9e42bc2c04b0765c8b89e0a9728c49ebf4676a92522b007af96ae9aec68d
|
||||
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v0.39.2@sha256:9fe32b5faf233bdbae27859f1c6aaa229230b0ac9ee5212a1ddf2763182cea0f
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/matchbox:v0.38.2@sha256:9cd7f46fcae119a3f8e35b428b018d0cb6da7b0cdd2ce764cc9fbf6dcd903f27
|
||||
ghcr.io/cozystack/cozystack/matchbox:v0.39.2@sha256:80da0287d6705ffaec918d1d7dc02f1d0a512045c56b78afb2707743d09e629e
|
||||
|
||||
@@ -12,6 +12,13 @@ spec:
|
||||
containers:
|
||||
- name: etcd-defrag
|
||||
image: ghcr.io/ahrtr/etcd-defrag:v0.13.0
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
args:
|
||||
- --endpoints={{ range $i, $e := until (int .Values.replicas) }}{{ if $i }},{{ end }}https://{{ $.Release.Name }}-{{ $i }}.{{ $.Release.Name }}-headless.{{ $.Release.Namespace }}.svc:2379{{ end }}
|
||||
- --cacert=/etc/etcd/pki/client/cert/ca.crt
|
||||
|
||||
@@ -55,25 +55,26 @@
|
||||
|
||||
### Alerta configuration
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| ----------------------------------------- | ----------------------------------------------------------------- | ---------- | ------- |
|
||||
| `alerta` | Configuration for the Alerta service. | `object` | `{}` |
|
||||
| `alerta.storage` | Persistent volume size for the database. | `string` | `10Gi` |
|
||||
| `alerta.storageClassName` | StorageClass used for the database. | `string` | `""` |
|
||||
| `alerta.resources` | Resource configuration. | `object` | `{}` |
|
||||
| `alerta.resources.requests` | Resource requests. | `object` | `{}` |
|
||||
| `alerta.resources.requests.cpu` | CPU request. | `quantity` | `100m` |
|
||||
| `alerta.resources.requests.memory` | Memory request. | `quantity` | `256Mi` |
|
||||
| `alerta.resources.limits` | Resource limits. | `object` | `{}` |
|
||||
| `alerta.resources.limits.cpu` | CPU limit. | `quantity` | `1` |
|
||||
| `alerta.resources.limits.memory` | Memory limit. | `quantity` | `1Gi` |
|
||||
| `alerta.alerts` | Alert routing configuration. | `object` | `{}` |
|
||||
| `alerta.alerts.telegram` | Configuration for Telegram alerts. | `object` | `{}` |
|
||||
| `alerta.alerts.telegram.token` | Telegram bot token. | `string` | `""` |
|
||||
| `alerta.alerts.telegram.chatID` | Telegram chat ID(s), separated by commas. | `string` | `""` |
|
||||
| `alerta.alerts.telegram.disabledSeverity` | List of severities without alerts (e.g. "informational,warning"). | `string` | `""` |
|
||||
| `alerta.alerts.slack` | Configuration for Slack alerts. | `object` | `{}` |
|
||||
| `alerta.alerts.slack.url` | Configuration uri for Slack alerts. | `string` | `""` |
|
||||
| Name | Description | Type | Value |
|
||||
| ----------------------------------------- | --------------------------------------------------------------------- | ---------- | ------- |
|
||||
| `alerta` | Configuration for the Alerta service. | `object` | `{}` |
|
||||
| `alerta.storage` | Persistent volume size for the database. | `string` | `10Gi` |
|
||||
| `alerta.storageClassName` | StorageClass used for the database. | `string` | `""` |
|
||||
| `alerta.resources` | Resource configuration. | `object` | `{}` |
|
||||
| `alerta.resources.requests` | Resource requests. | `object` | `{}` |
|
||||
| `alerta.resources.requests.cpu` | CPU request. | `quantity` | `100m` |
|
||||
| `alerta.resources.requests.memory` | Memory request. | `quantity` | `256Mi` |
|
||||
| `alerta.resources.limits` | Resource limits. | `object` | `{}` |
|
||||
| `alerta.resources.limits.cpu` | CPU limit. | `quantity` | `1` |
|
||||
| `alerta.resources.limits.memory` | Memory limit. | `quantity` | `1Gi` |
|
||||
| `alerta.alerts` | Alert routing configuration. | `object` | `{}` |
|
||||
| `alerta.alerts.telegram` | Configuration for Telegram alerts. | `object` | `{}` |
|
||||
| `alerta.alerts.telegram.token` | Telegram bot token. | `string` | `""` |
|
||||
| `alerta.alerts.telegram.chatID` | Telegram chat ID(s), separated by commas. | `string` | `""` |
|
||||
| `alerta.alerts.telegram.disabledSeverity` | List of severities without alerts (e.g. ["informational","warning"]). | `[]string` | `[]` |
|
||||
| `alerta.alerts.slack` | Configuration for Slack alerts. | `object` | `{}` |
|
||||
| `alerta.alerts.slack.url` | Configuration uri for Slack alerts. | `string` | `""` |
|
||||
| `alerta.alerts.slack.disabledSeverity` | List of severities without alerts (e.g. ["informational","warning"]). | `[]string` | `[]` |
|
||||
|
||||
|
||||
### Grafana configuration
|
||||
|
||||
@@ -129,13 +129,19 @@ spec:
|
||||
value: "{{ .Values.alerta.alerts.telegram.token }}"
|
||||
- name: TELEGRAM_WEBHOOK_URL
|
||||
value: "https://{{ printf "alerta.%s" (.Values.host | default $host) }}/api/webhooks/telegram?api-key={{ $apiKey }}"
|
||||
{{- if .Values.alerta.alerts.telegram.disabledSeverity }}
|
||||
- name: TELEGRAM_DISABLE_NOTIFICATION_SEVERITY
|
||||
value: "{{ .Values.alerta.alerts.telegram.disabledSeverity }}"
|
||||
value: {{ .Values.alerta.alerts.telegram.disabledSeverity | toJson | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.alerta.alerts.slack.url }}
|
||||
- name: "SLACK_WEBHOOK_URL"
|
||||
value: "{{ .Values.alerta.alerts.slack.url }}"
|
||||
{{- if .Values.alerta.alerts.slack.disabledSeverity }}
|
||||
- name: SLACK_SEVERITY_FILTER
|
||||
value: {{ .Values.alerta.alerts.slack.disabledSeverity | toJson | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
ports:
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"disabledSeverity": {
|
||||
"description": "List of severities without alerts (e.g. [\"informational\",\"warning\"]).",
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"description": "Configuration uri for Slack alerts.",
|
||||
"type": "string",
|
||||
@@ -42,9 +50,12 @@
|
||||
"default": ""
|
||||
},
|
||||
"disabledSeverity": {
|
||||
"description": "List of severities without alerts (e.g. \"informational,warning\").",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
"description": "List of severities without alerts (e.g. [\"informational\",\"warning\"]).",
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"token": {
|
||||
"description": "Telegram bot token.",
|
||||
|
||||
@@ -99,10 +99,11 @@ logsStorages:
|
||||
## @typedef {struct} TelegramAlerts - Telegram alert configuration.
|
||||
## @field {string} token - Telegram bot token.
|
||||
## @field {string} chatID - Telegram chat ID(s), separated by commas.
|
||||
## @field {string} [disabledSeverity] - List of severities without alerts (e.g. "informational,warning").
|
||||
## @field {[]string} [disabledSeverity] - List of severities without alerts (e.g. ["informational","warning"]).
|
||||
|
||||
## @typedef {struct} SlackAlerts - Slack alert configuration.
|
||||
## @field {string} url - Configuration uri for Slack alerts.
|
||||
## @field {[]string} [disabledSeverity] - List of severities without alerts (e.g. ["informational","warning"]).
|
||||
|
||||
## @typedef {struct} Alerts - Alert routing configuration.
|
||||
## @field {TelegramAlerts} [telegram] - Configuration for Telegram alerts.
|
||||
@@ -129,9 +130,10 @@ alerta:
|
||||
telegram:
|
||||
token: ""
|
||||
chatID: ""
|
||||
disabledSeverity: ""
|
||||
disabledSeverity: []
|
||||
slack:
|
||||
url: ""
|
||||
disabledSeverity: []
|
||||
##
|
||||
## @section Grafana configuration
|
||||
##
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.38.2@sha256:ff3281fe53a97d2cd5cd94bd4c4d8ff08189508729869bb39b3f60c80da5f919
|
||||
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.39.2@sha256:b180a6c4ce4d46cdf31af2731cc8b477e80ade99d4296d507ec5edb38c56a1ea
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
OUT=../../_out/repos/system
|
||||
CHARTS := $(shell find . -maxdepth 2 -name Chart.yaml | awk -F/ '{print $$2}')
|
||||
CHARTS := $(shell grep -F 'version: 0.0.0' */Chart.yaml | cut -f1 -d/)
|
||||
VERSIONED_CHARTS := $(shell grep '^version:' */Chart.yaml | grep -Fv '0.0.0' | cut -f1 -d/)
|
||||
|
||||
include ../../scripts/common-envs.mk
|
||||
|
||||
repo:
|
||||
rm -rf "$(OUT)"
|
||||
helm package -d "$(OUT)" $(CHARTS) --version $(COZYSTACK_VERSION)
|
||||
helm package -d "$(OUT)" $(VERSIONED_CHARTS)
|
||||
cd "$(OUT)" && helm repo index .
|
||||
|
||||
fix-charts:
|
||||
|
||||
3
packages/system/backup-controller/Chart.yaml
Normal file
3
packages/system/backup-controller/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
apiVersion: v2
|
||||
name: cozy-backup-controller
|
||||
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
|
||||
18
packages/system/backup-controller/Makefile
Normal file
18
packages/system/backup-controller/Makefile
Normal file
@@ -0,0 +1,18 @@
|
||||
NAME=backup-controller
|
||||
NAMESPACE=cozy-backup-controller
|
||||
|
||||
include ../../../scripts/common-envs.mk
|
||||
include ../../../scripts/package.mk
|
||||
|
||||
image: image-backup-controller
|
||||
|
||||
image-backup-controller:
|
||||
docker buildx build -f images/backup-controller/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/backup-controller:$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/backup-controller:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/backup-controller.json \
|
||||
$(BUILDX_ARGS)
|
||||
IMAGE="$(REGISTRY)/backup-controller:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/backup-controller.json -o json -r)" \
|
||||
yq -i '.backupController.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/backup-controller.json
|
||||
2
packages/system/backup-controller/definitions/.gitattributes
vendored
Normal file
2
packages/system/backup-controller/definitions/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
* linguist-generated
|
||||
.gitattributes -linguist-generated
|
||||
241
packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml
generated
Normal file
241
packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml
generated
Normal file
@@ -0,0 +1,241 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.16.4
|
||||
name: backupjobs.backups.cozystack.io
|
||||
spec:
|
||||
group: backups.cozystack.io
|
||||
names:
|
||||
kind: BackupJob
|
||||
listKind: BackupJobList
|
||||
plural: backupjobs
|
||||
singular: backupjob
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: |-
|
||||
BackupJob represents a single execution of a backup.
|
||||
It is typically created by a Plan controller when a schedule fires.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: BackupJobSpec describes the execution of a single backup
|
||||
operation.
|
||||
properties:
|
||||
applicationRef:
|
||||
description: |-
|
||||
ApplicationRef holds a reference to the managed application whose state
|
||||
is being backed up.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
planRef:
|
||||
description: |-
|
||||
PlanRef refers to the Plan that requested this backup run.
|
||||
For ad-hoc/manual backups, this can be omitted.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
storageRef:
|
||||
description: |-
|
||||
StorageRef holds a reference to the Storage object that describes where
|
||||
the backup will be stored.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
strategyRef:
|
||||
description: |-
|
||||
StrategyRef holds a reference to the driver-specific BackupStrategy object
|
||||
that describes how the backup should be created.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
required:
|
||||
- applicationRef
|
||||
- storageRef
|
||||
- strategyRef
|
||||
type: object
|
||||
status:
|
||||
description: BackupJobStatus represents the observed state of a BackupJob.
|
||||
properties:
|
||||
backupRef:
|
||||
description: BackupRef refers to the Backup object created by this
|
||||
run, if any.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
completedAt:
|
||||
description: |-
|
||||
CompletedAt is the time at which the backup run completed (successfully
|
||||
or otherwise).
|
||||
format: date-time
|
||||
type: string
|
||||
conditions:
|
||||
description: Conditions represents the latest available observations
|
||||
of a BackupJob's state.
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
message:
|
||||
description: |-
|
||||
Message is a human-readable message indicating details about why the
|
||||
backup run is in its current phase, if any.
|
||||
type: string
|
||||
phase:
|
||||
description: |-
|
||||
Phase is a high-level summary of the run's state.
|
||||
Typical values: Pending, Running, Succeeded, Failed.
|
||||
type: string
|
||||
startedAt:
|
||||
description: StartedAt is the time at which the backup run started.
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
selectableFields:
|
||||
- jsonPath: .spec.applicationRef.apiGroup
|
||||
- jsonPath: .spec.applicationRef.kind
|
||||
- jsonPath: .spec.applicationRef.name
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
238
packages/system/backup-controller/definitions/backups.cozystack.io_backups.yaml
generated
Normal file
238
packages/system/backup-controller/definitions/backups.cozystack.io_backups.yaml
generated
Normal file
@@ -0,0 +1,238 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.16.4
|
||||
name: backups.backups.cozystack.io
|
||||
spec:
|
||||
group: backups.cozystack.io
|
||||
names:
|
||||
kind: Backup
|
||||
listKind: BackupList
|
||||
plural: backups
|
||||
singular: backup
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Backup represents a single backup artifact for a given application.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: BackupSpec describes an immutable backup artifact produced
|
||||
by a BackupJob.
|
||||
properties:
|
||||
applicationRef:
|
||||
description: ApplicationRef refers to the application that was backed
|
||||
up.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
driverMetadata:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |-
|
||||
DriverMetadata holds driver-specific, opaque metadata associated with
|
||||
this backup (for example snapshot IDs, schema versions, etc.).
|
||||
This data is not interpreted by the core backup controllers.
|
||||
type: object
|
||||
planRef:
|
||||
description: |-
|
||||
PlanRef refers to the Plan that produced this backup, if any.
|
||||
For manually triggered backups, this can be omitted.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
storageRef:
|
||||
description: |-
|
||||
StorageRef refers to the Storage object that describes where the backup
|
||||
artifact is stored.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
strategyRef:
|
||||
description: |-
|
||||
StrategyRef refers to the driver-specific BackupStrategy that was used
|
||||
to create this backup. This allows the driver to later perform restores.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
takenAt:
|
||||
description: |-
|
||||
TakenAt is the time at which the backup was taken (as reported by the
|
||||
driver). It may differ slightly from metadata.creationTimestamp.
|
||||
format: date-time
|
||||
type: string
|
||||
required:
|
||||
- applicationRef
|
||||
- storageRef
|
||||
- strategyRef
|
||||
- takenAt
|
||||
type: object
|
||||
status:
|
||||
description: BackupStatus represents the observed state of a Backup.
|
||||
properties:
|
||||
artifact:
|
||||
description: Artifact describes the stored backup object, if available.
|
||||
properties:
|
||||
checksum:
|
||||
description: |-
|
||||
Checksum is the checksum of the artifact, if computed.
|
||||
For example: "sha256:<hex>".
|
||||
type: string
|
||||
sizeBytes:
|
||||
description: SizeBytes is the size of the artifact in bytes, if
|
||||
known.
|
||||
format: int64
|
||||
type: integer
|
||||
uri:
|
||||
description: |-
|
||||
URI is a driver-/storage-specific URI pointing to the backup artifact.
|
||||
For example: s3://bucket/prefix/file.tar.gz
|
||||
type: string
|
||||
required:
|
||||
- uri
|
||||
type: object
|
||||
conditions:
|
||||
description: Conditions represents the latest available observations
|
||||
of a Backup's state.
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
phase:
|
||||
description: |-
|
||||
Phase is a simple, high-level summary of the backup's state.
|
||||
Typical values are: Pending, Ready, Failed.
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
selectableFields:
|
||||
- jsonPath: .spec.applicationRef.apiGroup
|
||||
- jsonPath: .spec.applicationRef.kind
|
||||
- jsonPath: .spec.applicationRef.name
|
||||
served: true
|
||||
storage: true
|
||||
200
packages/system/backup-controller/definitions/backups.cozystack.io_plans.yaml
generated
Normal file
200
packages/system/backup-controller/definitions/backups.cozystack.io_plans.yaml
generated
Normal file
@@ -0,0 +1,200 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.16.4
|
||||
name: plans.backups.cozystack.io
|
||||
spec:
|
||||
group: backups.cozystack.io
|
||||
names:
|
||||
kind: Plan
|
||||
listKind: PlanList
|
||||
plural: plans
|
||||
singular: plan
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: |-
|
||||
Plan describes the schedule, method and storage location for the
|
||||
backup of a given target application.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: |-
|
||||
PlanSpec references the storage, the strategy, the application to be
|
||||
backed up and specifies the timetable on which the backups will run.
|
||||
properties:
|
||||
applicationRef:
|
||||
description: |-
|
||||
ApplicationRef holds a reference to the managed application,
|
||||
whose state and configuration must be backed up.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
schedule:
|
||||
description: Schedule specifies when backup copies are created.
|
||||
properties:
|
||||
cron:
|
||||
description: |-
|
||||
Cron contains the cron spec for scheduling backups. Must be
|
||||
specified if the schedule type is `cron`. Since only `cron` is
|
||||
supported, omitting this field is not allowed.
|
||||
type: string
|
||||
type:
|
||||
description: |-
|
||||
Type is the type of schedule specification. Supported values are
|
||||
[`cron`]. If omitted, defaults to `cron`.
|
||||
type: string
|
||||
type: object
|
||||
storageRef:
|
||||
description: |-
|
||||
StorageRef holds a reference to the Storage object that
|
||||
describes the location where the backup will be stored.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
strategyRef:
|
||||
description: |-
|
||||
StrategyRef holds a reference to the Strategy object that
|
||||
describes, how a backup copy is to be created.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
required:
|
||||
- applicationRef
|
||||
- schedule
|
||||
- storageRef
|
||||
- strategyRef
|
||||
type: object
|
||||
status:
|
||||
properties:
|
||||
conditions:
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
type: object
|
||||
selectableFields:
|
||||
- jsonPath: .spec.applicationRef.apiGroup
|
||||
- jsonPath: .spec.applicationRef.kind
|
||||
- jsonPath: .spec.applicationRef.name
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
174
packages/system/backup-controller/definitions/backups.cozystack.io_restorejobs.yaml
generated
Normal file
174
packages/system/backup-controller/definitions/backups.cozystack.io_restorejobs.yaml
generated
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.16.4
|
||||
name: restorejobs.backups.cozystack.io
|
||||
spec:
|
||||
group: backups.cozystack.io
|
||||
names:
|
||||
kind: RestoreJob
|
||||
listKind: RestoreJobList
|
||||
plural: restorejobs
|
||||
singular: restorejob
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.phase
|
||||
name: Phase
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: RestoreJob represents a single execution of a restore from a
|
||||
Backup.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: RestoreJobSpec describes the execution of a single restore
|
||||
operation.
|
||||
properties:
|
||||
backupRef:
|
||||
description: BackupRef refers to the Backup that should be restored.
|
||||
properties:
|
||||
name:
|
||||
default: ""
|
||||
description: |-
|
||||
Name of the referent.
|
||||
This field is effectively required, but due to backwards compatibility is
|
||||
allowed to be empty. Instances of this type with an empty value here are
|
||||
almost certainly wrong.
|
||||
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
targetApplicationRef:
|
||||
description: |-
|
||||
TargetApplicationRef refers to the application into which the backup
|
||||
should be restored. If omitted, the driver SHOULD restore into the same
|
||||
application as referenced by backup.spec.applicationRef.
|
||||
properties:
|
||||
apiGroup:
|
||||
description: |-
|
||||
APIGroup is the group for the resource being referenced.
|
||||
If APIGroup is not specified, the specified Kind must be in the core API group.
|
||||
For any other third-party types, APIGroup is required.
|
||||
type: string
|
||||
kind:
|
||||
description: Kind is the type of resource being referenced
|
||||
type: string
|
||||
name:
|
||||
description: Name is the name of resource being referenced
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
required:
|
||||
- backupRef
|
||||
type: object
|
||||
status:
|
||||
description: RestoreJobStatus represents the observed state of a RestoreJob.
|
||||
properties:
|
||||
completedAt:
|
||||
description: |-
|
||||
CompletedAt is the time at which the restore run completed (successfully
|
||||
or otherwise).
|
||||
format: date-time
|
||||
type: string
|
||||
conditions:
|
||||
description: Conditions represents the latest available observations
|
||||
of a RestoreJob's state.
|
||||
items:
|
||||
description: Condition contains details for one aspect of the current
|
||||
state of this API Resource.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: |-
|
||||
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: |-
|
||||
message is a human readable message indicating details about the transition.
|
||||
This may be an empty string.
|
||||
maxLength: 32768
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||
with respect to the current state of the instance.
|
||||
format: int64
|
||||
minimum: 0
|
||||
type: integer
|
||||
reason:
|
||||
description: |-
|
||||
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||
Producers of specific condition types may define expected values and meanings for this field,
|
||||
and whether the values are considered a guaranteed API.
|
||||
The value should be a CamelCase string.
|
||||
This field may not be empty.
|
||||
maxLength: 1024
|
||||
minLength: 1
|
||||
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||
type: string
|
||||
status:
|
||||
description: status of the condition, one of True, False, Unknown.
|
||||
enum:
|
||||
- "True"
|
||||
- "False"
|
||||
- Unknown
|
||||
type: string
|
||||
type:
|
||||
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||
maxLength: 316
|
||||
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||
type: string
|
||||
required:
|
||||
- lastTransitionTime
|
||||
- message
|
||||
- reason
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
message:
|
||||
description: |-
|
||||
Message is a human-readable message indicating details about why the
|
||||
restore run is in its current phase, if any.
|
||||
type: string
|
||||
phase:
|
||||
description: |-
|
||||
Phase is a high-level summary of the run's state.
|
||||
Typical values: Pending, Running, Succeeded, Failed.
|
||||
type: string
|
||||
startedAt:
|
||||
description: StartedAt is the time at which the restore run started.
|
||||
format: date-time
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -0,0 +1,23 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go mod download
|
||||
|
||||
COPY api api/
|
||||
COPY pkg pkg/
|
||||
COPY cmd cmd/
|
||||
COPY internal internal/
|
||||
|
||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /backup-controller cmd/backup-controller/main.go
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /backup-controller /backup-controller
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
ENTRYPOINT ["/backup-controller"]
|
||||
4
packages/system/backup-controller/templates/crds.yaml
Normal file
4
packages/system/backup-controller/templates/crds.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
{{- range $path, $_ := .Files.Glob "definitions/*.yaml" }}
|
||||
---
|
||||
{{ $.Files.Get $path }}
|
||||
{{- end }}
|
||||
57
packages/system/backup-controller/templates/deployment.yaml
Normal file
57
packages/system/backup-controller/templates/deployment.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: backup-controller
|
||||
labels:
|
||||
app: backup-controller
|
||||
spec:
|
||||
replicas: {{ .Values.backupController.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: backup-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: backup-controller
|
||||
spec:
|
||||
tolerations:
|
||||
- key: "node-role.kubernetes.io/control-plane"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
- key: "node-role.kubernetes.io/master"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
serviceAccountName: backup-controller
|
||||
containers:
|
||||
- name: backup-controller
|
||||
image: "{{ .Values.backupController.image }}"
|
||||
args:
|
||||
- --leader-elect
|
||||
{{- if .Values.backupController.metrics.enable }}
|
||||
- --metrics-bind-address={{ .Values.backupController.metrics.bindAddress }}
|
||||
{{- end }}
|
||||
{{- if .Values.backupController.debug }}
|
||||
- --zap-log-level=debug
|
||||
{{- else }}
|
||||
- --zap-log-level=info
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: metrics
|
||||
containerPort: {{ splitList ":" .Values.backupController.metrics.bindAddress | mustLast }}
|
||||
- name: health
|
||||
containerPort: 8081
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: health
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: health
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
{{- with .Values.backupController.resources }}
|
||||
resources: {{- . | toYaml | nindent 10 }}
|
||||
{{- end }}
|
||||
12
packages/system/backup-controller/templates/rbac-bind.yaml
Normal file
12
packages/system/backup-controller/templates/rbac-bind.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: backups.cozystack.io:core-controller
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: backups.cozystack.io:core-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: backup-controller
|
||||
namespace: {{ .Release.Namespace }}
|
||||
32
packages/system/backup-controller/templates/rbac.yaml
Normal file
32
packages/system/backup-controller/templates/rbac.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: backups.cozystack.io:core-controller
|
||||
rules:
|
||||
- apiGroups: ["backups.cozystack.io"]
|
||||
resources: ["plans"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["backups.cozystack.io"]
|
||||
resources: ["backupjobs"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch"]
|
||||
- apiGroups: ["backups.cozystack.io"]
|
||||
resources: ["backupjobs/status"]
|
||||
verbs: ["get", "update", "patch"]
|
||||
- apiGroups: ["backups.cozystack.io"]
|
||||
resources: ["backups"]
|
||||
verbs: ["create", "get", "list", "watch"]
|
||||
- apiGroups: ["apps.cozystack.io"]
|
||||
resources: ["buckets", "bucketaccesses", "virtualmachines"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["objectstorage.k8s.io"]
|
||||
resources: ["buckets", "bucketaccesses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch"]
|
||||
- apiGroups: ["kubevirt.io"]
|
||||
resources: ["virtualmachines"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["velero.io"]
|
||||
resources: ["backups", "backupstoragelocations", "volumesnapshotlocations", "restores"]
|
||||
verbs: ["create", "get", "list", "watch", "update", "patch"]
|
||||
4
packages/system/backup-controller/templates/sa.yaml
Normal file
4
packages/system/backup-controller/templates/sa.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
kind: ServiceAccount
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: backup-controller
|
||||
@@ -0,0 +1,5 @@
|
||||
apiVersion: strategy.backups.cozystack.io/v1alpha1
|
||||
kind: Velero
|
||||
metadata:
|
||||
name: velero-strategy-default
|
||||
spec: {}
|
||||
14
packages/system/backup-controller/values.yaml
Normal file
14
packages/system/backup-controller/values.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
backupController:
|
||||
image: ""
|
||||
replicas: 2
|
||||
debug: false
|
||||
metrics:
|
||||
enabled: true
|
||||
bindAddress: ":8443"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
3
packages/system/backupstrategy-controller/Chart.yaml
Normal file
3
packages/system/backupstrategy-controller/Chart.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
apiVersion: v2
|
||||
name: cozy-backup-controller
|
||||
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
|
||||
18
packages/system/backupstrategy-controller/Makefile
Normal file
18
packages/system/backupstrategy-controller/Makefile
Normal file
@@ -0,0 +1,18 @@
|
||||
NAME=backupstrategy-controller
|
||||
NAMESPACE=cozy-backup-controller
|
||||
|
||||
include ../../../scripts/common-envs.mk
|
||||
include ../../../scripts/package.mk
|
||||
|
||||
image: image-backupstrategy-controller
|
||||
|
||||
image-backupstrategy-controller:
|
||||
docker buildx build -f images/backupstrategy-controller/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/backupstrategy-controller:$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/backupstrategy-controller:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/backupstrategy-controller.json \
|
||||
$(BUILDX_ARGS)
|
||||
IMAGE="$(REGISTRY)/backupstrategy-controller:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/backupstrategy-controller.json -o json -r)" \
|
||||
yq -i '.backupStrategyController.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/backupstrategy-controller.json
|
||||
1
packages/system/backupstrategy-controller/definitions/.gitattributes
vendored
Normal file
1
packages/system/backupstrategy-controller/definitions/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.yaml linguist-generated
|
||||
8527
packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_jobs.yaml
generated
Normal file
8527
packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_jobs.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1025
packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml
generated
Normal file
1025
packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go mod download
|
||||
|
||||
COPY api api/
|
||||
COPY pkg pkg/
|
||||
COPY cmd cmd/
|
||||
COPY internal internal/
|
||||
|
||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /backupstrategy-controller cmd/backupstrategy-controller/main.go
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /backupstrategy-controller /backupstrategy-controller
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
ENTRYPOINT ["/backupstrategy-controller"]
|
||||
@@ -0,0 +1,4 @@
|
||||
{{- range $path, $_ := .Files.Glob "definitions/*.yaml" }}
|
||||
---
|
||||
{{ $.Files.Get $path }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,57 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: backupstrategy-controller
|
||||
labels:
|
||||
app: backupstrategy-controller
|
||||
spec:
|
||||
replicas: {{ .Values.backupStrategyController.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: backupstrategy-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: backupstrategy-controller
|
||||
spec:
|
||||
tolerations:
|
||||
- key: "node-role.kubernetes.io/control-plane"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
- key: "node-role.kubernetes.io/master"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
serviceAccountName: backupstrategy-controller
|
||||
containers:
|
||||
- name: backupstrategy-controller
|
||||
image: "{{ .Values.backupStrategyController.image }}"
|
||||
args:
|
||||
- --leader-elect
|
||||
{{- if .Values.backupStrategyController.metrics.enable }}
|
||||
- --metrics-bind-address={{ .Values.backupStrategyController.metrics.bindAddress }}
|
||||
{{- end }}
|
||||
{{- if .Values.backupStrategyController.debug }}
|
||||
- --zap-log-level=debug
|
||||
{{- else }}
|
||||
- --zap-log-level=info
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: metrics
|
||||
containerPort: {{ splitList ":" .Values.backupStrategyController.metrics.bindAddress | mustLast }}
|
||||
- name: health
|
||||
containerPort: 8081
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: health
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: health
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
{{- with .Values.backupStrategyController.resources }}
|
||||
resources: {{- . | toYaml | nindent 10 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: backups.cozystack.io:strategy-controller
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: backups.cozystack.io:strategy-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: backupstrategy-controller
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -0,0 +1,11 @@
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: backups.cozystack.io:strategy-controller
|
||||
rules:
|
||||
- apiGroups: ["strategy.backups.cozystack.io"]
|
||||
resources: ["*"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["backups.cozystack.io"]
|
||||
resources: ["backupjobs", "restorejobs"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
@@ -0,0 +1,4 @@
|
||||
kind: ServiceAccount
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: backupstrategy-controller
|
||||
14
packages/system/backupstrategy-controller/values.yaml
Normal file
14
packages/system/backupstrategy-controller/values.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
backupStrategyController:
|
||||
image: ""
|
||||
replicas: 2
|
||||
debug: false
|
||||
metrics:
|
||||
enabled: true
|
||||
bindAddress: ":8443"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:3825c9b4b6238f88f1b0de73bd18866a7e5f83f178d28fe2830f3bf24efb187d
|
||||
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:7dde8d748442c4457fb7bf67c0e0f4abf0da60af00a9171c6d4c0bfcc8a8b9c5
|
||||
|
||||
@@ -10,6 +10,7 @@ cilium:
|
||||
enabled: true
|
||||
loadBalancer:
|
||||
algorithm: maglev
|
||||
serviceTopology: true
|
||||
ipam:
|
||||
mode: "kubernetes"
|
||||
image:
|
||||
@@ -18,3 +19,6 @@ cilium:
|
||||
digest: "sha256:81262986a41487bfa3d0465091d3a386def5bd1ab476350bd4af2fdee5846fe6"
|
||||
envoy:
|
||||
enabled: false
|
||||
rollOutCiliumPods: true
|
||||
operator:
|
||||
rollOutPods: true
|
||||
|
||||
@@ -3,3 +3,6 @@ coredns:
|
||||
repository: registry.k8s.io/coredns/coredns
|
||||
tag: v1.12.4
|
||||
replicaCount: 2
|
||||
k8sAppLabelOverride: kube-dns
|
||||
service:
|
||||
name: kube-dns
|
||||
|
||||
@@ -21,6 +21,8 @@ spec:
|
||||
labels:
|
||||
app: cozystack-api
|
||||
spec:
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
serviceAccountName: cozystack-api
|
||||
{{- if .Values.cozystackAPI.localK8sAPIEndpoint.enabled }}
|
||||
nodeSelector:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cozystackAPI:
|
||||
image: ghcr.io/cozystack/cozystack/cozystack-api:v0.38.2@sha256:d17f1c59658731e5a2063c3db348adbc03b5cd31720052016b68449164cf2f14
|
||||
image: ghcr.io/cozystack/cozystack/cozystack-api:v0.39.2@sha256:e859dc81b4905fa163b75b1d567e91bd1eac572f85931954c2dc646cb07d3959
|
||||
localK8sAPIEndpoint:
|
||||
enabled: true
|
||||
replicas: 2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
cozystackController:
|
||||
image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.38.2@sha256:468b2eccbc0aa00bd3d72d56624a46e6ba178fa279cdd19248af74d32ea7d319
|
||||
image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.39.2@sha256:b96283a1774f23735849802069a907cb85f4fd57df89f8740a954a7d2922afc3
|
||||
debug: false
|
||||
disableTelemetry: false
|
||||
cozystackVersion: "v0.38.2"
|
||||
cozystackVersion: "v0.39.2"
|
||||
cozystackAPIKind: "DaemonSet"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{{- $brandingConfig:= lookup "v1" "ConfigMap" "cozy-system" "cozystack-branding" }}
|
||||
|
||||
{{- $tenantText := "v0.38.2" }}
|
||||
{{- $tenantText := "v0.39.2" }}
|
||||
{{- $footerText := "Cozystack" }}
|
||||
{{- $titleText := "Cozystack Dashboard" }}
|
||||
{{- $logoText := "" }}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user