mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-06 15:08:53 +00:00
Compare commits
90 Commits
remove-ass
...
backport-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d841940af9 | ||
|
|
ea466820fc | ||
|
|
31422aba38 | ||
|
|
8f6a09bca5 | ||
|
|
d13a45c8d6 | ||
|
|
6b3cd40ba5 | ||
|
|
838d1abff6 | ||
|
|
4cdd1113d1 | ||
|
|
4d7ee809e5 | ||
|
|
125a57eb97 | ||
|
|
d33a2357ed | ||
|
|
6ce60917c7 | ||
|
|
a8a7e510ac | ||
|
|
cf0598bcf1 | ||
|
|
6169220317 | ||
|
|
fbe33e1191 | ||
|
|
9507f24332 | ||
|
|
2aa839b68f | ||
|
|
40683e28a7 | ||
|
|
4ddb374680 | ||
|
|
e07fafb393 | ||
|
|
7b932a400d | ||
|
|
8f317c9065 | ||
|
|
e73bf9905d | ||
|
|
c5222aae97 | ||
|
|
e176cdec87 | ||
|
|
477d391440 | ||
|
|
9ba76d4839 | ||
|
|
f5ccbe94f9 | ||
|
|
f900db6338 | ||
|
|
3bcc0e5cc0 | ||
|
|
2a8317291d | ||
|
|
acc6ed26bb | ||
|
|
3af7430074 | ||
|
|
800ca3b3d2 | ||
|
|
4a83d2c7c8 | ||
|
|
c0b1539d3e | ||
|
|
1c42f8bd10 | ||
|
|
de2bbd35c2 | ||
|
|
fe82f66330 | ||
|
|
c4bf72b44f | ||
|
|
33452d65e5 | ||
|
|
9039011252 | ||
|
|
2bbeb1b474 | ||
|
|
a51e2078a8 | ||
|
|
e40a95849d | ||
|
|
31bcbbd5bf | ||
|
|
e7b62ca3c8 | ||
|
|
868a7497d0 | ||
|
|
666eeffa47 | ||
|
|
8d166e0754 | ||
|
|
967f0bdc9f | ||
|
|
30c1041e7a | ||
|
|
68a639b32c | ||
|
|
2bf1168dcf | ||
|
|
526af29459 | ||
|
|
b65fc64e17 | ||
|
|
9a2d39a4b0 | ||
|
|
fe565aff4a | ||
|
|
29bd12d52d | ||
|
|
12023b7f02 | ||
|
|
f3c178e30d | ||
|
|
654eb7c3f9 | ||
|
|
ffd10e99f1 | ||
|
|
f400310f28 | ||
|
|
9ab7430cec | ||
|
|
87fa8c93fc | ||
|
|
d1c0af1db4 | ||
|
|
eb042b4ef1 | ||
|
|
ecb9a223aa | ||
|
|
77de2bdda0 | ||
|
|
b3179d032a | ||
|
|
a939b2bbd0 | ||
|
|
682f83e71c | ||
|
|
66cfbe7c0e | ||
|
|
67109c33b1 | ||
|
|
f9bfcfd125 | ||
|
|
1beb11a6a9 | ||
|
|
f7c6a54b0c | ||
|
|
35c57798a6 | ||
|
|
aba147571b | ||
|
|
17138d825d | ||
|
|
f5eb211312 | ||
|
|
c44b198a60 | ||
|
|
9b5f3726b6 | ||
|
|
66b68e3798 | ||
|
|
bd443bd578 | ||
|
|
2c37611136 | ||
|
|
a4a432e2aa | ||
|
|
da6c053d3f |
13
Makefile
13
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: manifests assets unit-tests helm-unit-tests
|
||||
.PHONY: manifests repos assets unit-tests helm-unit-tests
|
||||
|
||||
build-deps:
|
||||
@command -V find docker skopeo jq gh helm > /dev/null
|
||||
@@ -15,7 +15,6 @@ 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/linstor image
|
||||
@@ -26,15 +25,21 @@ build: build-deps
|
||||
make -C packages/system/kamaji image
|
||||
make -C packages/system/bucket image
|
||||
make -C packages/system/objectstorage-controller image
|
||||
make -C packages/system/grafana-operator image
|
||||
make -C packages/core/testing image
|
||||
make -C packages/core/talos image
|
||||
make -C packages/core/platform image
|
||||
make -C packages/core/installer image
|
||||
make manifests
|
||||
|
||||
repos:
|
||||
rm -rf _out
|
||||
make -C packages/system repo
|
||||
make -C packages/apps repo
|
||||
make -C packages/extra repo
|
||||
|
||||
manifests:
|
||||
mkdir -p _out/assets
|
||||
(cd packages/core/installer/; helm template --namespace cozy-installer installer .) > _out/assets/cozystack-installer.yaml
|
||||
(cd packages/core/installer/; helm template -n cozy-installer installer .) > _out/assets/cozystack-installer.yaml
|
||||
|
||||
assets:
|
||||
make -C packages/core/talos assets
|
||||
|
||||
1
api/.gitattributes
vendored
1
api/.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
zz_generated_deepcopy.go linguist-generated
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// 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"`
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// 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"`
|
||||
}
|
||||
|
||||
type VeleroStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
//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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -1,421 +0,0 @@
|
||||
# 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.
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
// 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"`
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// 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"`
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// 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"`
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// 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
|
||||
|
||||
// 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"`
|
||||
}
|
||||
@@ -1,501 +0,0 @@
|
||||
//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
|
||||
}
|
||||
@@ -17,7 +17,6 @@ limitations under the License.
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -62,6 +61,24 @@ type CozystackResourceDefinitionSpec struct {
|
||||
Dashboard *CozystackResourceDefinitionDashboard `json:"dashboard,omitempty"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionChart struct {
|
||||
// Name of the Helm chart
|
||||
Name string `json:"name"`
|
||||
// Source reference for the Helm chart
|
||||
SourceRef SourceRef `json:"sourceRef"`
|
||||
}
|
||||
|
||||
type SourceRef struct {
|
||||
// Kind of the source reference
|
||||
// +kubebuilder:default:="HelmRepository"
|
||||
Kind string `json:"kind"`
|
||||
// Name of the source reference
|
||||
Name string `json:"name"`
|
||||
// Namespace of the source reference
|
||||
// +kubebuilder:default:="cozy-public"
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionApplication struct {
|
||||
// Kind of the application, used for UI and API
|
||||
Kind string `json:"kind"`
|
||||
@@ -74,8 +91,8 @@ type CozystackResourceDefinitionApplication struct {
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionRelease struct {
|
||||
// Reference to the chart source
|
||||
ChartRef *helmv2.CrossNamespaceSourceReference `json:"chartRef"`
|
||||
// Helm chart configuration
|
||||
Chart CozystackResourceDefinitionChart `json:"chart"`
|
||||
// Labels for the release
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Prefix for the release name
|
||||
@@ -93,18 +110,17 @@ type CozystackResourceDefinitionRelease struct {
|
||||
// - {{ .namespace }}: The namespace of the resource being processed
|
||||
//
|
||||
// Example YAML:
|
||||
//
|
||||
// secrets:
|
||||
// include:
|
||||
// - matchExpressions:
|
||||
// - key: badlabel
|
||||
// operator: DoesNotExist
|
||||
// matchLabels:
|
||||
// goodlabel: goodvalue
|
||||
// resourceNames:
|
||||
// - "{{ .name }}-secret"
|
||||
// - "{{ .kind }}-{{ .name }}-tls"
|
||||
// - "specificname"
|
||||
// secrets:
|
||||
// include:
|
||||
// - matchExpressions:
|
||||
// - key: badlabel
|
||||
// operator: DoesNotExist
|
||||
// matchLabels:
|
||||
// goodlabel: goodvalue
|
||||
// resourceNames:
|
||||
// - "{{ .name }}-secret"
|
||||
// - "{{ .kind }}-{{ .name }}-tls"
|
||||
// - "specificname"
|
||||
type CozystackResourceDefinitionResourceSelector struct {
|
||||
metav1.LabelSelector `json:",inline"`
|
||||
// ResourceNames is a list of resource names to match
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
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
|
||||
|
||||
import (
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster,shortName={pkg,pkgs}
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Variant",type="string",JSONPath=".spec.variant",description="Selected variant"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message",description="Ready message"
|
||||
|
||||
// Package is the Schema for the packages API
|
||||
type Package struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec PackageSpec `json:"spec,omitempty"`
|
||||
Status PackageStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PackageList contains a list of Packages
|
||||
type PackageList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Package `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Package{}, &PackageList{})
|
||||
}
|
||||
|
||||
// PackageSpec defines the desired state of Package
|
||||
type PackageSpec struct {
|
||||
// Variant is the name of the variant to use from the PackageSource
|
||||
// If not specified, defaults to "default"
|
||||
// +optional
|
||||
Variant string `json:"variant,omitempty"`
|
||||
|
||||
// IgnoreDependencies is a list of package source dependencies to ignore
|
||||
// Dependencies listed here will not be installed even if they are specified in the PackageSource
|
||||
// +optional
|
||||
IgnoreDependencies []string `json:"ignoreDependencies,omitempty"`
|
||||
|
||||
// Components is a map of release name to component overrides
|
||||
// Allows overriding values and enabling/disabling specific components from the PackageSource
|
||||
// +optional
|
||||
Components map[string]PackageComponent `json:"components,omitempty"`
|
||||
}
|
||||
|
||||
// PackageComponent defines overrides for a specific component
|
||||
type PackageComponent struct {
|
||||
// Enabled indicates whether this component should be installed
|
||||
// If false, the component will be disabled even if it's defined in the PackageSource
|
||||
// +optional
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
|
||||
// Values contains Helm chart values as a JSON object
|
||||
// These values will be merged with the default values from the PackageSource
|
||||
// +optional
|
||||
Values *apiextensionsv1.JSON `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
// PackageStatus defines the observed state of Package
|
||||
type PackageStatus struct {
|
||||
// Conditions represents the latest available observations of a Package's state
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
|
||||
// Dependencies tracks the readiness status of each dependency
|
||||
// Key is the dependency package name, value indicates if the dependency is ready
|
||||
// +optional
|
||||
Dependencies map[string]DependencyStatus `json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
// DependencyStatus represents the readiness status of a dependency
|
||||
type DependencyStatus struct {
|
||||
// Ready indicates whether the dependency is ready
|
||||
Ready bool `json:"ready"`
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
/*
|
||||
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
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster,shortName={pks}
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Variants",type="string",JSONPath=".status.variants",description="Package variants (comma-separated)"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message",description="Ready message"
|
||||
|
||||
// PackageSource is the Schema for the packagesources API
|
||||
type PackageSource struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec PackageSourceSpec `json:"spec,omitempty"`
|
||||
Status PackageSourceStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PackageSourceList contains a list of PackageSources
|
||||
type PackageSourceList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []PackageSource `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&PackageSource{}, &PackageSourceList{})
|
||||
}
|
||||
|
||||
// PackageSourceSpec defines the desired state of PackageSource
|
||||
type PackageSourceSpec struct {
|
||||
// SourceRef is the source reference for the package source charts
|
||||
// +optional
|
||||
SourceRef *PackageSourceRef `json:"sourceRef,omitempty"`
|
||||
|
||||
// Variants is a list of package source variants
|
||||
// Each variant defines components, applications, dependencies, and libraries for a specific configuration
|
||||
// +optional
|
||||
Variants []Variant `json:"variants,omitempty"`
|
||||
}
|
||||
|
||||
// Variant defines a single variant configuration
|
||||
type Variant struct {
|
||||
// Name is the unique identifier for this variant
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// DependsOn is a list of package source dependencies
|
||||
// For example: "cozystack.networking"
|
||||
// +optional
|
||||
DependsOn []string `json:"dependsOn,omitempty"`
|
||||
|
||||
// Libraries is a list of Helm library charts used by components in this variant
|
||||
// +optional
|
||||
Libraries []Library `json:"libraries,omitempty"`
|
||||
|
||||
// Components is a list of Helm releases to be installed as part of this variant
|
||||
// +optional
|
||||
Components []Component `json:"components,omitempty"`
|
||||
}
|
||||
|
||||
// Library defines a Helm library chart
|
||||
type Library struct {
|
||||
// Name is the optional name for library placed in charts
|
||||
// +optional
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// Path is the path to the library chart directory
|
||||
// +required
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// PackageSourceRef defines the source reference for package source charts
|
||||
type PackageSourceRef struct {
|
||||
// Kind of the source reference
|
||||
// +kubebuilder:validation:Enum=GitRepository;OCIRepository
|
||||
// +required
|
||||
Kind string `json:"kind"`
|
||||
|
||||
// Name of the source reference
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// Namespace of the source reference
|
||||
// +required
|
||||
Namespace string `json:"namespace"`
|
||||
|
||||
// Path is the base path where packages are located in the source.
|
||||
// For GitRepository, defaults to "packages" if not specified.
|
||||
// For OCIRepository, defaults to empty string (root) if not specified.
|
||||
// +optional
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// ComponentInstall defines installation parameters for a component
|
||||
type ComponentInstall struct {
|
||||
// ReleaseName is the name of the HelmRelease resource that will be created
|
||||
// If not specified, defaults to the component Name field
|
||||
// +optional
|
||||
ReleaseName string `json:"releaseName,omitempty"`
|
||||
|
||||
// Namespace is the Kubernetes namespace where the release will be installed
|
||||
// +optional
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
|
||||
// Privileged indicates whether this release requires privileged access
|
||||
// +optional
|
||||
Privileged bool `json:"privileged,omitempty"`
|
||||
|
||||
// DependsOn is a list of component names that must be installed before this component
|
||||
// +optional
|
||||
DependsOn []string `json:"dependsOn,omitempty"`
|
||||
}
|
||||
|
||||
// Component defines a single Helm release component within a package source
|
||||
type Component struct {
|
||||
// Name is the unique identifier for this component within the package source
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path is the path to the Helm chart directory
|
||||
// +required
|
||||
Path string `json:"path"`
|
||||
|
||||
// Install defines installation parameters for this component
|
||||
// +optional
|
||||
Install *ComponentInstall `json:"install,omitempty"`
|
||||
|
||||
// Libraries is a list of library names that this component depends on
|
||||
// These libraries must be defined at the variant level
|
||||
// +optional
|
||||
Libraries []string `json:"libraries,omitempty"`
|
||||
|
||||
// ValuesFiles is a list of values file names to use
|
||||
// +optional
|
||||
ValuesFiles []string `json:"valuesFiles,omitempty"`
|
||||
}
|
||||
|
||||
// PackageSourceStatus defines the observed state of PackageSource
|
||||
type PackageSourceStatus struct {
|
||||
// Variants is a comma-separated list of package variant names
|
||||
// This field is populated by the controller based on spec.variants keys
|
||||
// +optional
|
||||
Variants string `json:"variants,omitempty"`
|
||||
|
||||
// Conditions represents the latest available observations of a PackageSource's state
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
@@ -21,63 +21,10 @@ limitations under the License.
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"github.com/fluxcd/helm-controller/api/v2"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Component) DeepCopyInto(out *Component) {
|
||||
*out = *in
|
||||
if in.Install != nil {
|
||||
in, out := &in.Install, &out.Install
|
||||
*out = new(ComponentInstall)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Libraries != nil {
|
||||
in, out := &in.Libraries, &out.Libraries
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.ValuesFiles != nil {
|
||||
in, out := &in.ValuesFiles, &out.ValuesFiles
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Component.
|
||||
func (in *Component) DeepCopy() *Component {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Component)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ComponentInstall) DeepCopyInto(out *ComponentInstall) {
|
||||
*out = *in
|
||||
if in.DependsOn != nil {
|
||||
in, out := &in.DependsOn, &out.DependsOn
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentInstall.
|
||||
func (in *ComponentInstall) DeepCopy() *ComponentInstall {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ComponentInstall)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinition) DeepCopyInto(out *CozystackResourceDefinition) {
|
||||
*out = *in
|
||||
@@ -119,6 +66,22 @@ func (in *CozystackResourceDefinitionApplication) DeepCopy() *CozystackResourceD
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionChart) DeepCopyInto(out *CozystackResourceDefinitionChart) {
|
||||
*out = *in
|
||||
out.SourceRef = in.SourceRef
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionChart.
|
||||
func (in *CozystackResourceDefinitionChart) DeepCopy() *CozystackResourceDefinitionChart {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionChart)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionDashboard) DeepCopyInto(out *CozystackResourceDefinitionDashboard) {
|
||||
*out = *in
|
||||
@@ -190,11 +153,7 @@ func (in *CozystackResourceDefinitionList) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionRelease) DeepCopyInto(out *CozystackResourceDefinitionRelease) {
|
||||
*out = *in
|
||||
if in.ChartRef != nil {
|
||||
in, out := &in.ChartRef, &out.ChartRef
|
||||
*out = new(v2.CrossNamespaceSourceReference)
|
||||
**out = **in
|
||||
}
|
||||
out.Chart = in.Chart
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
@@ -297,299 +256,6 @@ func (in *CozystackResourceDefinitionSpec) DeepCopy() *CozystackResourceDefiniti
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DependencyStatus) DeepCopyInto(out *DependencyStatus) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyStatus.
|
||||
func (in *DependencyStatus) DeepCopy() *DependencyStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DependencyStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Library) DeepCopyInto(out *Library) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Library.
|
||||
func (in *Library) DeepCopy() *Library {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Library)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Package) DeepCopyInto(out *Package) {
|
||||
*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 Package.
|
||||
func (in *Package) DeepCopy() *Package {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Package)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Package) 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 *PackageComponent) DeepCopyInto(out *PackageComponent) {
|
||||
*out = *in
|
||||
if in.Enabled != nil {
|
||||
in, out := &in.Enabled, &out.Enabled
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.Values != nil {
|
||||
in, out := &in.Values, &out.Values
|
||||
*out = new(v1.JSON)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageComponent.
|
||||
func (in *PackageComponent) DeepCopy() *PackageComponent {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageComponent)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageList) DeepCopyInto(out *PackageList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Package, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageList.
|
||||
func (in *PackageList) DeepCopy() *PackageList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PackageList) 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 *PackageSource) DeepCopyInto(out *PackageSource) {
|
||||
*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 PackageSource.
|
||||
func (in *PackageSource) DeepCopy() *PackageSource {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSource)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PackageSource) 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 *PackageSourceList) DeepCopyInto(out *PackageSourceList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]PackageSource, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceList.
|
||||
func (in *PackageSourceList) DeepCopy() *PackageSourceList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSourceList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PackageSourceList) 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 *PackageSourceRef) DeepCopyInto(out *PackageSourceRef) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceRef.
|
||||
func (in *PackageSourceRef) DeepCopy() *PackageSourceRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSourceRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageSourceSpec) DeepCopyInto(out *PackageSourceSpec) {
|
||||
*out = *in
|
||||
if in.SourceRef != nil {
|
||||
in, out := &in.SourceRef, &out.SourceRef
|
||||
*out = new(PackageSourceRef)
|
||||
**out = **in
|
||||
}
|
||||
if in.Variants != nil {
|
||||
in, out := &in.Variants, &out.Variants
|
||||
*out = make([]Variant, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceSpec.
|
||||
func (in *PackageSourceSpec) DeepCopy() *PackageSourceSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSourceSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageSourceStatus) DeepCopyInto(out *PackageSourceStatus) {
|
||||
*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 PackageSourceStatus.
|
||||
func (in *PackageSourceStatus) DeepCopy() *PackageSourceStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSourceStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageSpec) DeepCopyInto(out *PackageSpec) {
|
||||
*out = *in
|
||||
if in.IgnoreDependencies != nil {
|
||||
in, out := &in.IgnoreDependencies, &out.IgnoreDependencies
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Components != nil {
|
||||
in, out := &in.Components, &out.Components
|
||||
*out = make(map[string]PackageComponent, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = *val.DeepCopy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSpec.
|
||||
func (in *PackageSpec) DeepCopy() *PackageSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageStatus) DeepCopyInto(out *PackageStatus) {
|
||||
*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])
|
||||
}
|
||||
}
|
||||
if in.Dependencies != nil {
|
||||
in, out := &in.Dependencies, &out.Dependencies
|
||||
*out = make(map[string]DependencyStatus, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageStatus.
|
||||
func (in *PackageStatus) DeepCopy() *PackageStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in Selector) DeepCopyInto(out *Selector) {
|
||||
{
|
||||
@@ -612,33 +278,16 @@ func (in Selector) DeepCopy() Selector {
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Variant) DeepCopyInto(out *Variant) {
|
||||
func (in *SourceRef) DeepCopyInto(out *SourceRef) {
|
||||
*out = *in
|
||||
if in.DependsOn != nil {
|
||||
in, out := &in.DependsOn, &out.DependsOn
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Libraries != nil {
|
||||
in, out := &in.Libraries, &out.Libraries
|
||||
*out = make([]Library, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Components != nil {
|
||||
in, out := &in.Components, &out.Components
|
||||
*out = make([]Component, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Variant.
|
||||
func (in *Variant) DeepCopy() *Variant {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceRef.
|
||||
func (in *SourceRef) DeepCopy() *SourceRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Variant)
|
||||
out := new(SourceRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
/*
|
||||
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: "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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// +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)
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
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: "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(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Job")
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var addCmdFlags struct {
|
||||
files []string
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add [package]...",
|
||||
Short: "Install PackageSource and its dependencies interactively",
|
||||
Long: `Install PackageSource and its dependencies interactively.
|
||||
|
||||
You can specify packages as arguments or use -f flag to read from files.
|
||||
Multiple -f flags can be specified, and they can point to files or directories.`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Collect package names from arguments and files
|
||||
packageNames := make(map[string]bool)
|
||||
packagesFromFiles := make(map[string]string) // packageName -> filePath
|
||||
|
||||
for _, arg := range args {
|
||||
packageNames[arg] = true
|
||||
}
|
||||
|
||||
// Read packages from files
|
||||
for _, filePath := range addCmdFlags.files {
|
||||
packages, err := readPackagesFromFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
packageNames[pkg] = true
|
||||
if oldPath, ok := packagesFromFiles[pkg]; ok {
|
||||
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
|
||||
}
|
||||
packagesFromFiles[pkg] = filePath
|
||||
}
|
||||
}
|
||||
|
||||
if len(packageNames) == 0 {
|
||||
return fmt.Errorf("no packages specified")
|
||||
}
|
||||
|
||||
// Create Kubernetes client config
|
||||
var config *rest.Config
|
||||
var err error
|
||||
|
||||
if addCmdFlags.kubeconfig != "" {
|
||||
config, err = clientcmd.BuildConfigFromFlags("", addCmdFlags.kubeconfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load kubeconfig from %s: %w", addCmdFlags.kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
config, err = ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
|
||||
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
// Process each package
|
||||
for packageName := range packageNames {
|
||||
// Check if package comes from a file
|
||||
if filePath, fromFile := packagesFromFiles[packageName]; fromFile {
|
||||
// Try to create Package directly from file
|
||||
if err := createPackageFromFile(ctx, k8sClient, filePath, packageName); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", packageName)
|
||||
continue
|
||||
}
|
||||
// If failed, fall back to interactive installation
|
||||
}
|
||||
|
||||
// Interactive installation from PackageSource
|
||||
if err := installPackage(ctx, k8sClient, packageName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func readPackagesFromFile(filePath string) ([]string, error) {
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var packages []string
|
||||
|
||||
if info.IsDir() {
|
||||
// Read all YAML files from directory
|
||||
err := filepath.Walk(filePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() || !strings.HasSuffix(path, ".yaml") && !strings.HasSuffix(path, ".yml") {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkgs, err := readPackagesFromYAMLFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read %s: %w", path, err)
|
||||
}
|
||||
packages = append(packages, pkgs...)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
packages, err = readPackagesFromYAMLFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
func readPackagesFromYAMLFile(filePath string) ([]string, error) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var packages []string
|
||||
|
||||
// Split YAML documents (in case of multiple resources)
|
||||
documents := strings.Split(string(data), "---")
|
||||
|
||||
for _, doc := range documents {
|
||||
doc = strings.TrimSpace(doc)
|
||||
if doc == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse using Kubernetes decoder
|
||||
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
obj := &unstructured.Unstructured{}
|
||||
_, _, err := decoder.Decode([]byte(doc), nil, obj)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a Package
|
||||
if obj.GetKind() == "Package" {
|
||||
name := obj.GetName()
|
||||
if name != "" {
|
||||
packages = append(packages, name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a PackageSource
|
||||
if obj.GetKind() == "PackageSource" {
|
||||
name := obj.GetName()
|
||||
if name != "" {
|
||||
packages = append(packages, name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to parse as PackageList or PackageSourceList
|
||||
if obj.GetKind() == "PackageList" || obj.GetKind() == "PackageSourceList" {
|
||||
items, found, err := unstructured.NestedSlice(obj.Object, "items")
|
||||
if err == nil && found {
|
||||
for _, item := range items {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if metadata, ok := itemMap["metadata"].(map[string]interface{}); ok {
|
||||
if name, ok := metadata["name"].(string); ok && name != "" {
|
||||
packages = append(packages, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Return empty list if no packages found - don't error out
|
||||
// The check for whether any packages were specified at all is handled later in RunE
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
// buildDependencyTree builds a dependency tree starting from the root PackageSource
|
||||
// Returns both the dependency tree and a map of dependencies to their requesters
|
||||
func buildDependencyTree(ctx context.Context, k8sClient client.Client, rootName string) (map[string][]string, map[string]string, error) {
|
||||
tree := make(map[string][]string)
|
||||
dependencyRequesters := make(map[string]string) // dep -> requester
|
||||
visited := make(map[string]bool)
|
||||
|
||||
// Ensure root is in tree even if it has no dependencies
|
||||
tree[rootName] = []string{}
|
||||
|
||||
var buildTree func(string) error
|
||||
buildTree = func(pkgName string) error {
|
||||
if visited[pkgName] {
|
||||
return nil
|
||||
}
|
||||
visited[pkgName] = true
|
||||
|
||||
// Get PackageSource
|
||||
ps := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: pkgName}, ps); err != nil {
|
||||
// If PackageSource doesn't exist, just skip it
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect all dependencies from all variants
|
||||
deps := make(map[string]bool)
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, dep := range variant.DependsOn {
|
||||
deps[dep] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependencies to tree
|
||||
for dep := range deps {
|
||||
if _, exists := tree[pkgName]; !exists {
|
||||
tree[pkgName] = []string{}
|
||||
}
|
||||
tree[pkgName] = append(tree[pkgName], dep)
|
||||
// Track who requested this dependency
|
||||
dependencyRequesters[dep] = pkgName
|
||||
// Recursively build tree for dependencies
|
||||
if err := buildTree(dep); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := buildTree(rootName); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return tree, dependencyRequesters, nil
|
||||
}
|
||||
|
||||
// topologicalSort performs topological sort on the dependency tree
|
||||
// Returns order from root to leaves (dependencies first)
|
||||
func topologicalSort(tree map[string][]string) ([]string, error) {
|
||||
// Build reverse graph (dependencies -> dependents)
|
||||
reverseGraph := make(map[string][]string)
|
||||
allNodes := make(map[string]bool)
|
||||
|
||||
for node, deps := range tree {
|
||||
allNodes[node] = true
|
||||
for _, dep := range deps {
|
||||
allNodes[dep] = true
|
||||
reverseGraph[dep] = append(reverseGraph[dep], node)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate in-degrees (how many dependencies a node has)
|
||||
inDegree := make(map[string]int)
|
||||
for node := range allNodes {
|
||||
inDegree[node] = 0
|
||||
}
|
||||
for node, deps := range tree {
|
||||
inDegree[node] = len(deps)
|
||||
}
|
||||
|
||||
// Kahn's algorithm - start with nodes that have no dependencies
|
||||
var queue []string
|
||||
for node, degree := range inDegree {
|
||||
if degree == 0 {
|
||||
queue = append(queue, node)
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for len(queue) > 0 {
|
||||
node := queue[0]
|
||||
queue = queue[1:]
|
||||
result = append(result, node)
|
||||
|
||||
// Process dependents (nodes that depend on this node)
|
||||
for _, dependent := range reverseGraph[node] {
|
||||
inDegree[dependent]--
|
||||
if inDegree[dependent] == 0 {
|
||||
queue = append(queue, dependent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cycles
|
||||
if len(result) != len(allNodes) {
|
||||
return nil, fmt.Errorf("dependency cycle detected")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// createPackageFromFile creates a Package resource directly from a YAML file
|
||||
func createPackageFromFile(ctx context.Context, k8sClient client.Client, filePath string, packageName string) error {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Split YAML documents
|
||||
documents := strings.Split(string(data), "---")
|
||||
|
||||
for _, doc := range documents {
|
||||
doc = strings.TrimSpace(doc)
|
||||
if doc == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse using Kubernetes decoder
|
||||
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
obj := &unstructured.Unstructured{}
|
||||
_, _, err := decoder.Decode([]byte(doc), nil, obj)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a Package with matching name
|
||||
if obj.GetKind() == "Package" && obj.GetName() == packageName {
|
||||
// Convert to Package
|
||||
var pkg cozyv1alpha1.Package
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &pkg); err != nil {
|
||||
return fmt.Errorf("failed to convert Package: %w", err)
|
||||
}
|
||||
|
||||
// Create Package
|
||||
if err := k8sClient.Create(ctx, &pkg); err != nil {
|
||||
return fmt.Errorf("failed to create Package: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Package %s not found in file", packageName)
|
||||
}
|
||||
|
||||
func installPackage(ctx context.Context, k8sClient client.Client, packageSourceName string) error {
|
||||
// Get PackageSource
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: packageSourceName}, packageSource); err != nil {
|
||||
return fmt.Errorf("failed to get PackageSource %s: %w", packageSourceName, err)
|
||||
}
|
||||
|
||||
// Build dependency tree
|
||||
dependencyTree, dependencyRequesters, err := buildDependencyTree(ctx, k8sClient, packageSourceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build dependency tree: %w", err)
|
||||
}
|
||||
|
||||
// Topological sort (install from root to leaves)
|
||||
installOrder, err := topologicalSort(dependencyTree)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sort dependencies: %w", err)
|
||||
}
|
||||
|
||||
// Get all PackageSources for variant selection
|
||||
var allPackageSources cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &allPackageSources); err != nil {
|
||||
return fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
packageSourceMap := make(map[string]*cozyv1alpha1.PackageSource)
|
||||
for i := range allPackageSources.Items {
|
||||
packageSourceMap[allPackageSources.Items[i].Name] = &allPackageSources.Items[i]
|
||||
}
|
||||
|
||||
// Get all installed Packages
|
||||
var installedPackages cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &installedPackages); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
installedMap := make(map[string]*cozyv1alpha1.Package)
|
||||
for i := range installedPackages.Items {
|
||||
installedMap[installedPackages.Items[i].Name] = &installedPackages.Items[i]
|
||||
}
|
||||
|
||||
// First, collect all variant selections
|
||||
fmt.Fprintf(os.Stderr, "Installing %s and its dependencies...\n\n", packageSourceName)
|
||||
packageVariants := make(map[string]string) // packageName -> variant
|
||||
|
||||
for _, pkgName := range installOrder {
|
||||
// Check if already installed
|
||||
if installed, exists := installedMap[pkgName]; exists {
|
||||
variant := installed.Spec.Variant
|
||||
if variant == "" {
|
||||
variant = "default"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "✓ %s (already installed, variant: %s)\n", pkgName, variant)
|
||||
packageVariants[pkgName] = variant
|
||||
continue
|
||||
}
|
||||
|
||||
// Get PackageSource for this dependency
|
||||
ps, exists := packageSourceMap[pkgName]
|
||||
if !exists {
|
||||
requester := dependencyRequesters[pkgName]
|
||||
if requester != "" {
|
||||
return fmt.Errorf("PackageSource %s not found (required by %s)", pkgName, requester)
|
||||
}
|
||||
return fmt.Errorf("PackageSource %s not found", pkgName)
|
||||
}
|
||||
|
||||
// Select variant interactively
|
||||
variant, err := selectVariantInteractive(ps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to select variant for %s: %w", pkgName, err)
|
||||
}
|
||||
|
||||
packageVariants[pkgName] = variant
|
||||
}
|
||||
|
||||
// Now create all Package resources
|
||||
for _, pkgName := range installOrder {
|
||||
// Skip if already installed
|
||||
if _, exists := installedMap[pkgName]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
variant := packageVariants[pkgName]
|
||||
|
||||
// Create Package
|
||||
pkg := &cozyv1alpha1.Package{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: pkgName,
|
||||
},
|
||||
Spec: cozyv1alpha1.PackageSpec{
|
||||
Variant: variant,
|
||||
},
|
||||
}
|
||||
|
||||
if err := k8sClient.Create(ctx, pkg); err != nil {
|
||||
return fmt.Errorf("failed to create Package %s: %w", pkgName, err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", pkgName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectVariantInteractive prompts user to select a variant
|
||||
func selectVariantInteractive(ps *cozyv1alpha1.PackageSource) (string, error) {
|
||||
if len(ps.Spec.Variants) == 0 {
|
||||
return "", fmt.Errorf("no variants available for PackageSource %s", ps.Name)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nPackageSource: %s\n", ps.Name)
|
||||
fmt.Fprintf(os.Stderr, "Available variants:\n")
|
||||
for i, variant := range ps.Spec.Variants {
|
||||
fmt.Fprintf(os.Stderr, " %d. %s\n", i+1, variant.Name)
|
||||
}
|
||||
|
||||
// If only one variant, use it as default
|
||||
defaultVariant := ps.Spec.Variants[0].Name
|
||||
var prompt string
|
||||
if len(ps.Spec.Variants) == 1 {
|
||||
prompt = "Select variant [1]: "
|
||||
} else {
|
||||
prompt = fmt.Sprintf("Select variant (1-%d): ", len(ps.Spec.Variants))
|
||||
}
|
||||
|
||||
for {
|
||||
fmt.Fprintf(os.Stderr, prompt)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read input: %w", err)
|
||||
}
|
||||
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
// If input is empty and there's a default variant, use it
|
||||
if input == "" && len(ps.Spec.Variants) == 1 {
|
||||
return defaultVariant, nil
|
||||
}
|
||||
|
||||
choice, err := strconv.Atoi(input)
|
||||
if err != nil || choice < 1 || choice > len(ps.Spec.Variants) {
|
||||
fmt.Fprintf(os.Stderr, "Invalid choice. Please enter a number between 1 and %d.\n", len(ps.Spec.Variants))
|
||||
continue
|
||||
}
|
||||
|
||||
return ps.Spec.Variants[choice-1].Name, nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(addCmd)
|
||||
addCmd.Flags().StringArrayVarP(&addCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
|
||||
addCmd.Flags().StringVar(&addCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
|
||||
}
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var delCmdFlags struct {
|
||||
files []string
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
var delCmd = &cobra.Command{
|
||||
Use: "del [package]...",
|
||||
Short: "Delete Package resources",
|
||||
Long: `Delete Package resources.
|
||||
|
||||
You can specify packages as arguments or use -f flag to read from files.
|
||||
Multiple -f flags can be specified, and they can point to files or directories.`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Collect package names from arguments and files
|
||||
packageNames := make(map[string]bool)
|
||||
packagesFromFiles := make(map[string]string) // packageName -> filePath
|
||||
|
||||
for _, arg := range args {
|
||||
packageNames[arg] = true
|
||||
}
|
||||
|
||||
// Read packages from files (reuse function from add.go)
|
||||
for _, filePath := range delCmdFlags.files {
|
||||
packages, err := readPackagesFromFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
packageNames[pkg] = true
|
||||
if oldPath, ok := packagesFromFiles[pkg]; ok {
|
||||
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
|
||||
}
|
||||
packagesFromFiles[pkg] = filePath
|
||||
}
|
||||
}
|
||||
|
||||
if len(packageNames) == 0 {
|
||||
return fmt.Errorf("no packages specified")
|
||||
}
|
||||
|
||||
// Create Kubernetes client config
|
||||
var config *rest.Config
|
||||
var err error
|
||||
|
||||
if delCmdFlags.kubeconfig != "" {
|
||||
config, err = clientcmd.BuildConfigFromFlags("", delCmdFlags.kubeconfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load kubeconfig from %s: %w", delCmdFlags.kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
config, err = ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
|
||||
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
// Check which requested packages are installed
|
||||
var installedPackages cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &installedPackages); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
installedMap := make(map[string]bool)
|
||||
for _, pkg := range installedPackages.Items {
|
||||
installedMap[pkg.Name] = true
|
||||
}
|
||||
|
||||
// Warn about requested packages that are not installed
|
||||
for pkgName := range packageNames {
|
||||
if !installedMap[pkgName] {
|
||||
fmt.Fprintf(os.Stderr, "⚠ Package %s is not installed, skipping\n", pkgName)
|
||||
}
|
||||
}
|
||||
|
||||
// Find all packages to delete (including dependents)
|
||||
packagesToDelete, err := findPackagesToDelete(ctx, k8sClient, packageNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to analyze dependencies: %w", err)
|
||||
}
|
||||
|
||||
if len(packagesToDelete) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "No packages found to delete\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Show packages to be deleted and ask for confirmation
|
||||
if err := confirmDeletion(packagesToDelete, packageNames); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete packages in reverse topological order (dependents first, then dependencies)
|
||||
deleteOrder, err := getDeleteOrder(ctx, k8sClient, packagesToDelete)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine delete order: %w", err)
|
||||
}
|
||||
|
||||
// Delete each package
|
||||
for _, packageName := range deleteOrder {
|
||||
pkg := &cozyv1alpha1.Package{}
|
||||
pkg.Name = packageName
|
||||
if err := k8sClient.Delete(ctx, pkg); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
fmt.Fprintf(os.Stderr, "⚠ Package %s not found, skipping\n", packageName)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to delete Package %s: %w", packageName, err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "✓ Deleted Package %s\n", packageName)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// findPackagesToDelete finds all packages that need to be deleted, including dependents
|
||||
func findPackagesToDelete(ctx context.Context, k8sClient client.Client, requestedPackages map[string]bool) (map[string]bool, error) {
|
||||
// Get all installed Packages
|
||||
var installedPackages cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &installedPackages); err != nil {
|
||||
return nil, fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
installedMap := make(map[string]bool)
|
||||
for _, pkg := range installedPackages.Items {
|
||||
installedMap[pkg.Name] = true
|
||||
}
|
||||
|
||||
// Get all PackageSources to build dependency graph
|
||||
var packageSources cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &packageSources); err != nil {
|
||||
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
// Build reverse dependency graph (dependents -> dependencies)
|
||||
// This tells us: for each package, which packages depend on it
|
||||
reverseDeps := make(map[string][]string)
|
||||
for _, ps := range packageSources.Items {
|
||||
// Only consider installed packages
|
||||
if !installedMap[ps.Name] {
|
||||
continue
|
||||
}
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Only consider installed dependencies
|
||||
if installedMap[dep] {
|
||||
reverseDeps[dep] = append(reverseDeps[dep], ps.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all packages to delete (requested + their dependents)
|
||||
packagesToDelete := make(map[string]bool)
|
||||
visited := make(map[string]bool)
|
||||
|
||||
var findDependents func(string)
|
||||
findDependents = func(pkgName string) {
|
||||
if visited[pkgName] {
|
||||
return
|
||||
}
|
||||
visited[pkgName] = true
|
||||
|
||||
// Only add if it's installed
|
||||
if installedMap[pkgName] {
|
||||
packagesToDelete[pkgName] = true
|
||||
}
|
||||
|
||||
// Recursively find all dependents
|
||||
for _, dependent := range reverseDeps[pkgName] {
|
||||
if installedMap[dependent] {
|
||||
findDependents(dependent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start from requested packages
|
||||
for pkgName := range requestedPackages {
|
||||
if !installedMap[pkgName] {
|
||||
continue
|
||||
}
|
||||
findDependents(pkgName)
|
||||
}
|
||||
|
||||
return packagesToDelete, nil
|
||||
}
|
||||
|
||||
// confirmDeletion shows the list of packages to be deleted and asks for user confirmation
|
||||
func confirmDeletion(packagesToDelete map[string]bool, requestedPackages map[string]bool) error {
|
||||
// Separate requested packages from dependents
|
||||
var requested []string
|
||||
var dependents []string
|
||||
|
||||
for pkg := range packagesToDelete {
|
||||
if requestedPackages[pkg] {
|
||||
requested = append(requested, pkg)
|
||||
} else {
|
||||
dependents = append(dependents, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nThe following packages will be deleted:\n\n")
|
||||
|
||||
if len(requested) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Requested packages:\n")
|
||||
for _, pkg := range requested {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
|
||||
if len(dependents) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Dependent packages (will also be deleted):\n")
|
||||
for _, pkg := range dependents {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Total: %d package(s)\n\n", len(packagesToDelete))
|
||||
fmt.Fprintf(os.Stderr, "Do you want to continue? [y/N]: ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read input: %w", err)
|
||||
}
|
||||
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
if input != "y" && input != "yes" {
|
||||
return fmt.Errorf("deletion cancelled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDeleteOrder returns packages in reverse topological order (dependents first, then dependencies)
|
||||
// This ensures we delete dependents before their dependencies
|
||||
func getDeleteOrder(ctx context.Context, k8sClient client.Client, packagesToDelete map[string]bool) ([]string, error) {
|
||||
// Get all PackageSources to build dependency graph
|
||||
var packageSources cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &packageSources); err != nil {
|
||||
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
// Build forward dependency graph (package -> dependencies)
|
||||
dependencyGraph := make(map[string][]string)
|
||||
for _, ps := range packageSources.Items {
|
||||
if !packagesToDelete[ps.Name] {
|
||||
continue
|
||||
}
|
||||
deps := make(map[string]bool)
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, dep := range variant.DependsOn {
|
||||
if packagesToDelete[dep] {
|
||||
deps[dep] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var depList []string
|
||||
for dep := range deps {
|
||||
depList = append(depList, dep)
|
||||
}
|
||||
dependencyGraph[ps.Name] = depList
|
||||
}
|
||||
|
||||
// Build reverse graph for topological sort
|
||||
reverseGraph := make(map[string][]string)
|
||||
allNodes := make(map[string]bool)
|
||||
|
||||
for node, deps := range dependencyGraph {
|
||||
allNodes[node] = true
|
||||
for _, dep := range deps {
|
||||
allNodes[dep] = true
|
||||
reverseGraph[dep] = append(reverseGraph[dep], node)
|
||||
}
|
||||
}
|
||||
|
||||
// Add nodes that have no dependencies
|
||||
for pkg := range packagesToDelete {
|
||||
if !allNodes[pkg] {
|
||||
allNodes[pkg] = true
|
||||
dependencyGraph[pkg] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate in-degrees
|
||||
inDegree := make(map[string]int)
|
||||
for node := range allNodes {
|
||||
inDegree[node] = 0
|
||||
}
|
||||
for node, deps := range dependencyGraph {
|
||||
inDegree[node] = len(deps)
|
||||
}
|
||||
|
||||
// Kahn's algorithm - start with nodes that have no dependencies
|
||||
var queue []string
|
||||
for node, degree := range inDegree {
|
||||
if degree == 0 {
|
||||
queue = append(queue, node)
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for len(queue) > 0 {
|
||||
node := queue[0]
|
||||
queue = queue[1:]
|
||||
result = append(result, node)
|
||||
|
||||
// Process dependents
|
||||
for _, dependent := range reverseGraph[node] {
|
||||
inDegree[dependent]--
|
||||
if inDegree[dependent] == 0 {
|
||||
queue = append(queue, dependent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cycles: if not all nodes were processed, there's a cycle
|
||||
if len(result) != len(allNodes) {
|
||||
// Find unprocessed nodes
|
||||
processed := make(map[string]bool)
|
||||
for _, node := range result {
|
||||
processed[node] = true
|
||||
}
|
||||
var unprocessed []string
|
||||
for node := range allNodes {
|
||||
if !processed[node] {
|
||||
unprocessed = append(unprocessed, node)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("dependency cycle detected: the following packages form a cycle and cannot be deleted: %v", unprocessed)
|
||||
}
|
||||
|
||||
// Reverse the result to get dependents first, then dependencies
|
||||
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(delCmd)
|
||||
delCmd.Flags().StringArrayVarP(&delCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
|
||||
delCmd.Flags().StringVar(&delCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
|
||||
}
|
||||
|
||||
@@ -1,564 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"github.com/emicklei/dot"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
var dotCmdFlags struct {
|
||||
installed bool
|
||||
components bool
|
||||
files []string
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
var dotCmd = &cobra.Command{
|
||||
Use: "dot [package]...",
|
||||
Short: "Generate dependency graph as graphviz DOT format",
|
||||
Long: `Generate dependency graph as graphviz DOT format.
|
||||
|
||||
Pipe the output through the "dot" program (part of graphviz package) to render the graph:
|
||||
|
||||
cozypkg dot | dot -Tpng > graph.png
|
||||
|
||||
By default, shows dependencies for all PackageSource resources.
|
||||
Use --installed to show only installed Package resources.
|
||||
Specify packages as arguments or use -f flag to read from files.`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Collect package names from arguments and files
|
||||
packageNames := make(map[string]bool)
|
||||
for _, arg := range args {
|
||||
packageNames[arg] = true
|
||||
}
|
||||
|
||||
// Read packages from files (reuse function from add.go)
|
||||
for _, filePath := range dotCmdFlags.files {
|
||||
packages, err := readPackagesFromFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
packageNames[pkg] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to slice, empty means all packages
|
||||
var selectedPackages []string
|
||||
if len(packageNames) > 0 {
|
||||
for pkg := range packageNames {
|
||||
selectedPackages = append(selectedPackages, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
// If multiple packages specified, show graph for all of them
|
||||
// If single package, use packageName for backward compatibility
|
||||
var packageName string
|
||||
if len(selectedPackages) == 1 {
|
||||
packageName = selectedPackages[0]
|
||||
} else if len(selectedPackages) > 1 {
|
||||
// Multiple packages - pass empty string to packageName, use selectedPackages
|
||||
packageName = ""
|
||||
}
|
||||
|
||||
// packagesOnly is inverse of components flag (if components=false, then packagesOnly=true)
|
||||
packagesOnly := !dotCmdFlags.components
|
||||
graph, allNodes, edgeVariants, packageNames, err := buildGraphFromCluster(ctx, dotCmdFlags.kubeconfig, packagesOnly, dotCmdFlags.installed, packageName, selectedPackages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting PackageSource dependencies: %w", err)
|
||||
}
|
||||
|
||||
dotGraph := generateDOTGraph(graph, allNodes, packagesOnly, edgeVariants, packageNames)
|
||||
dotGraph.Write(os.Stdout)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(dotCmd)
|
||||
dotCmd.Flags().BoolVarP(&dotCmdFlags.installed, "installed", "i", false, "show dependencies only for installed Package resources")
|
||||
dotCmd.Flags().BoolVar(&dotCmdFlags.components, "components", false, "show component-level dependencies")
|
||||
dotCmd.Flags().StringArrayVarP(&dotCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
|
||||
dotCmd.Flags().StringVar(&dotCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
|
||||
}
|
||||
|
||||
var (
|
||||
dependenciesScheme = runtime.NewScheme()
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(dependenciesScheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(dependenciesScheme))
|
||||
}
|
||||
|
||||
// buildGraphFromCluster builds a dependency graph from PackageSource resources in the cluster.
|
||||
// Returns: graph, allNodes, edgeVariants (map[edgeKey]variants), packageNames, error
|
||||
func buildGraphFromCluster(ctx context.Context, kubeconfig string, packagesOnly bool, installedOnly bool, packageName string, selectedPackages []string) (map[string][]string, map[string]bool, map[string][]string, map[string]bool, error) {
|
||||
// Create Kubernetes client config
|
||||
var config *rest.Config
|
||||
var err error
|
||||
|
||||
if kubeconfig != "" {
|
||||
// Load kubeconfig from explicit path
|
||||
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to load kubeconfig from %s: %w", kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
// Use default kubeconfig loading (from env var or ~/.kube/config)
|
||||
config, err = ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
k8sClient, err := client.New(config, client.Options{Scheme: dependenciesScheme})
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
// Get installed Packages if needed
|
||||
installedPackages := make(map[string]bool)
|
||||
if installedOnly {
|
||||
var packageList cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &packageList); err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
for _, pkg := range packageList.Items {
|
||||
installedPackages[pkg.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// List all PackageSource resources
|
||||
var packageSourceList cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &packageSourceList); err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
// Build map of existing packages and components
|
||||
packageNames := make(map[string]bool)
|
||||
allExistingComponents := make(map[string]bool) // "package.component" -> true
|
||||
for _, ps := range packageSourceList.Items {
|
||||
if ps.Name != "" {
|
||||
packageNames[ps.Name] = true
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, component := range variant.Components {
|
||||
if component.Install != nil {
|
||||
componentFullName := fmt.Sprintf("%s.%s", ps.Name, component.Name)
|
||||
allExistingComponents[componentFullName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graph := make(map[string][]string)
|
||||
allNodes := make(map[string]bool)
|
||||
edgeVariants := make(map[string][]string) // key: "source->target", value: list of variant names
|
||||
existingEdges := make(map[string]bool) // key: "source->target" to avoid duplicates
|
||||
componentHasLocalDeps := make(map[string]bool) // componentName -> has local component dependencies
|
||||
|
||||
// Process each PackageSource
|
||||
for _, ps := range packageSourceList.Items {
|
||||
psName := ps.Name
|
||||
if psName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by package name if specified
|
||||
if packageName != "" && psName != packageName {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by selected packages if specified
|
||||
if len(selectedPackages) > 0 {
|
||||
found := false
|
||||
for _, selected := range selectedPackages {
|
||||
if psName == selected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by installed packages if flag is set
|
||||
if installedOnly && !installedPackages[psName] {
|
||||
continue
|
||||
}
|
||||
|
||||
allNodes[psName] = true
|
||||
|
||||
// Track package dependencies per variant
|
||||
packageDepVariants := make(map[string]map[string]bool) // dep -> variant -> true
|
||||
allVariantNames := make(map[string]bool)
|
||||
for _, v := range ps.Spec.Variants {
|
||||
allVariantNames[v.Name] = true
|
||||
}
|
||||
|
||||
// Track component dependencies per variant
|
||||
componentDepVariants := make(map[string]map[string]map[string]bool) // componentName -> dep -> variant -> true
|
||||
componentVariants := make(map[string]map[string]bool) // componentName -> variant -> true
|
||||
|
||||
// Extract dependencies from variants
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
// Variant-level dependencies (package-level)
|
||||
for _, dep := range variant.DependsOn {
|
||||
// If installedOnly is set, only include dependencies that are installed
|
||||
if installedOnly && !installedPackages[dep] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Track which variant this dependency comes from
|
||||
if packageDepVariants[dep] == nil {
|
||||
packageDepVariants[dep] = make(map[string]bool)
|
||||
}
|
||||
packageDepVariants[dep][variant.Name] = true
|
||||
|
||||
edgeKey := fmt.Sprintf("%s->%s", psName, dep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[psName] = append(graph[psName], dep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
|
||||
// Add to allNodes only if package exists
|
||||
if packageNames[dep] {
|
||||
allNodes[dep] = true
|
||||
}
|
||||
// If package doesn't exist, don't add to allNodes - it will be shown as missing (red)
|
||||
}
|
||||
|
||||
// Component-level dependencies
|
||||
if !packagesOnly {
|
||||
for _, component := range variant.Components {
|
||||
// Skip components without install section
|
||||
if component.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
componentName := fmt.Sprintf("%s.%s", psName, component.Name)
|
||||
allNodes[componentName] = true
|
||||
|
||||
// Track which variants this component appears in
|
||||
if componentVariants[componentName] == nil {
|
||||
componentVariants[componentName] = make(map[string]bool)
|
||||
}
|
||||
componentVariants[componentName][variant.Name] = true
|
||||
|
||||
if component.Install != nil {
|
||||
if componentDepVariants[componentName] == nil {
|
||||
componentDepVariants[componentName] = make(map[string]map[string]bool)
|
||||
}
|
||||
|
||||
for _, dep := range component.Install.DependsOn {
|
||||
// Track which variant this dependency comes from
|
||||
if componentDepVariants[componentName][dep] == nil {
|
||||
componentDepVariants[componentName][dep] = make(map[string]bool)
|
||||
}
|
||||
componentDepVariants[componentName][dep][variant.Name] = true
|
||||
|
||||
// Check if it's a local component dependency or external
|
||||
if strings.Contains(dep, ".") {
|
||||
// External component dependency (package.component format)
|
||||
// Mark that this component has local dependencies (for edge to package logic)
|
||||
componentHasLocalDeps[componentName] = true
|
||||
|
||||
// Check if target component exists
|
||||
if allExistingComponents[dep] {
|
||||
// Component exists
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, dep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], dep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
allNodes[dep] = true
|
||||
} else {
|
||||
// Component doesn't exist - create missing component node
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, dep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], dep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
// Don't add to allNodes - will be shown as missing (red)
|
||||
|
||||
// Add edge from missing component to its package
|
||||
parts := strings.SplitN(dep, ".", 2)
|
||||
if len(parts) == 2 {
|
||||
depPackageName := parts[0]
|
||||
missingEdgeKey := fmt.Sprintf("%s->%s", dep, depPackageName)
|
||||
if !existingEdges[missingEdgeKey] {
|
||||
graph[dep] = append(graph[dep], depPackageName)
|
||||
existingEdges[missingEdgeKey] = true
|
||||
}
|
||||
// Add package to allNodes only if it exists
|
||||
if packageNames[depPackageName] {
|
||||
allNodes[depPackageName] = true
|
||||
}
|
||||
// If package doesn't exist, it will be shown as missing (red)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local component dependency (same package)
|
||||
// Mark that this component has local dependencies
|
||||
componentHasLocalDeps[componentName] = true
|
||||
|
||||
localDep := fmt.Sprintf("%s.%s", psName, dep)
|
||||
|
||||
// Check if target component exists
|
||||
if allExistingComponents[localDep] {
|
||||
// Component exists
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, localDep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], localDep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
allNodes[localDep] = true
|
||||
} else {
|
||||
// Component doesn't exist - create missing component node
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, localDep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], localDep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
// Don't add to allNodes - will be shown as missing (red)
|
||||
|
||||
// Add edge from missing component to its package
|
||||
missingEdgeKey := fmt.Sprintf("%s->%s", localDep, psName)
|
||||
if !existingEdges[missingEdgeKey] {
|
||||
graph[localDep] = append(graph[localDep], psName)
|
||||
existingEdges[missingEdgeKey] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store variant information for package dependencies that are not in all variants
|
||||
for dep, variants := range packageDepVariants {
|
||||
if len(variants) < len(allVariantNames) {
|
||||
var variantList []string
|
||||
for v := range variants {
|
||||
variantList = append(variantList, v)
|
||||
}
|
||||
edgeKey := fmt.Sprintf("%s->%s", psName, dep)
|
||||
edgeVariants[edgeKey] = variantList
|
||||
}
|
||||
}
|
||||
|
||||
// Add component->package edges for components without local dependencies
|
||||
if !packagesOnly {
|
||||
for componentName := range componentVariants {
|
||||
// Only add edge to package if component has no local component dependencies
|
||||
if !componentHasLocalDeps[componentName] {
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, psName)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], psName)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
|
||||
// If component is not in all variants, store variant info for component->package edge
|
||||
componentAllVariants := componentVariants[componentName]
|
||||
if len(componentAllVariants) < len(allVariantNames) {
|
||||
var variantList []string
|
||||
for v := range componentAllVariants {
|
||||
variantList = append(variantList, v)
|
||||
}
|
||||
edgeVariants[edgeKey] = variantList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store variant information for component dependencies that are not in all variants
|
||||
for componentName, deps := range componentDepVariants {
|
||||
componentAllVariants := componentVariants[componentName]
|
||||
for dep, variants := range deps {
|
||||
if len(variants) < len(componentAllVariants) {
|
||||
var variantList []string
|
||||
for v := range variants {
|
||||
variantList = append(variantList, v)
|
||||
}
|
||||
// Determine the actual target name
|
||||
var targetName string
|
||||
if strings.Contains(dep, ".") {
|
||||
targetName = dep
|
||||
} else {
|
||||
targetName = fmt.Sprintf("%s.%s", psName, dep)
|
||||
}
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, targetName)
|
||||
edgeVariants[edgeKey] = variantList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return graph, allNodes, edgeVariants, packageNames, nil
|
||||
}
|
||||
|
||||
// generateDOTGraph generates a DOT graph from the dependency graph.
|
||||
func generateDOTGraph(graph map[string][]string, allNodes map[string]bool, packagesOnly bool, edgeVariants map[string][]string, packageNames map[string]bool) *dot.Graph {
|
||||
g := dot.NewGraph(dot.Directed)
|
||||
g.Attr("rankdir", "RL")
|
||||
g.Attr("nodesep", "0.5")
|
||||
g.Attr("ranksep", "1.0")
|
||||
|
||||
// Helper function to check if a node is a package
|
||||
// A node is a package if:
|
||||
// 1. It's directly in packageNames
|
||||
// 2. It doesn't contain a dot (simple package name)
|
||||
// 3. It contains a dot but the part before the first dot is a package name
|
||||
isPackage := func(nodeName string) bool {
|
||||
if packageNames[nodeName] {
|
||||
return true
|
||||
}
|
||||
if !strings.Contains(nodeName, ".") {
|
||||
return true
|
||||
}
|
||||
// If it contains a dot, check if the part before the first dot is a package
|
||||
parts := strings.SplitN(nodeName, ".", 2)
|
||||
if len(parts) > 0 {
|
||||
return packageNames[parts[0]]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Add nodes
|
||||
for node := range allNodes {
|
||||
if packagesOnly && !isPackage(node) {
|
||||
// Skip component nodes when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
n := g.Node(node)
|
||||
|
||||
// Style nodes based on type
|
||||
if isPackage(node) {
|
||||
// Package node
|
||||
n.Attr("shape", "box")
|
||||
n.Attr("style", "rounded,filled")
|
||||
n.Attr("fillcolor", "lightblue")
|
||||
n.Attr("label", node)
|
||||
} else {
|
||||
// Component node
|
||||
n.Attr("shape", "box")
|
||||
n.Attr("style", "rounded,filled")
|
||||
n.Attr("fillcolor", "lightyellow")
|
||||
// Extract component name (part after last dot)
|
||||
parts := strings.Split(node, ".")
|
||||
if len(parts) > 0 {
|
||||
n.Attr("label", parts[len(parts)-1])
|
||||
} else {
|
||||
n.Attr("label", node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add edges
|
||||
for source, targets := range graph {
|
||||
if packagesOnly && !isPackage(source) {
|
||||
// Skip component edges when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
if packagesOnly && !isPackage(target) {
|
||||
// Skip component edges when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if target exists
|
||||
targetExists := allNodes[target]
|
||||
|
||||
// Determine edge type for coloring
|
||||
sourceIsPackage := isPackage(source)
|
||||
targetIsPackage := isPackage(target)
|
||||
|
||||
// Add edge
|
||||
edge := g.Edge(g.Node(source), g.Node(target))
|
||||
|
||||
// Set edge color based on type (if target exists)
|
||||
if targetExists {
|
||||
if sourceIsPackage && targetIsPackage {
|
||||
// Package -> Package: black (default)
|
||||
edge.Attr("color", "black")
|
||||
} else {
|
||||
// Component -> Package or Component -> Component: green
|
||||
edge.Attr("color", "green")
|
||||
}
|
||||
}
|
||||
|
||||
// If target doesn't exist, mark it as missing (red color)
|
||||
if !targetExists {
|
||||
edge.Attr("color", "red")
|
||||
edge.Attr("style", "dashed")
|
||||
|
||||
// Also add the missing node with red color
|
||||
missingNode := g.Node(target)
|
||||
missingNode.Attr("shape", "box")
|
||||
missingNode.Attr("style", "rounded,filled,dashed")
|
||||
missingNode.Attr("fillcolor", "lightcoral")
|
||||
|
||||
// Determine label based on node type
|
||||
if isPackage(target) {
|
||||
// Package node
|
||||
missingNode.Attr("label", target)
|
||||
} else {
|
||||
// Component node - extract component name
|
||||
parts := strings.Split(target, ".")
|
||||
if len(parts) > 0 {
|
||||
missingNode.Attr("label", parts[len(parts)-1])
|
||||
} else {
|
||||
missingNode.Attr("label", target)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if this edge has variant information (dependency not in all variants)
|
||||
edgeKey := fmt.Sprintf("%s->%s", source, target)
|
||||
if variants, hasVariants := edgeVariants[edgeKey]; hasVariants {
|
||||
// Add label with variant names
|
||||
edge.Attr("label", strings.Join(variants, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var listCmdFlags struct {
|
||||
installed bool
|
||||
components bool
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List PackageSource or Package resources",
|
||||
Long: `List PackageSource or Package resources in table format.
|
||||
|
||||
By default, lists PackageSource resources. Use --installed flag to list installed Package resources.
|
||||
Use --components flag to show components on separate lines.`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create Kubernetes client config
|
||||
var config *rest.Config
|
||||
var err error
|
||||
|
||||
if listCmdFlags.kubeconfig != "" {
|
||||
config, err = clientcmd.BuildConfigFromFlags("", listCmdFlags.kubeconfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load kubeconfig from %s: %w", listCmdFlags.kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
config, err = ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
|
||||
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
if listCmdFlags.installed {
|
||||
return listPackages(ctx, k8sClient, listCmdFlags.components)
|
||||
}
|
||||
return listPackageSources(ctx, k8sClient, listCmdFlags.components)
|
||||
},
|
||||
}
|
||||
|
||||
func listPackageSources(ctx context.Context, k8sClient client.Client, showComponents bool) error {
|
||||
var psList cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &psList); err != nil {
|
||||
return fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
// Use tabwriter for better column alignment
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||
defer w.Flush()
|
||||
|
||||
// Print header
|
||||
fmt.Fprintln(w, "NAME\tVARIANTS\tREADY\tSTATUS")
|
||||
|
||||
// Print rows
|
||||
for _, ps := range psList.Items {
|
||||
// Get variants
|
||||
var variants []string
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
variants = append(variants, variant.Name)
|
||||
}
|
||||
variantsStr := strings.Join(variants, ",")
|
||||
if len(variantsStr) > 28 {
|
||||
variantsStr = variantsStr[:25] + "..."
|
||||
}
|
||||
|
||||
// Get Ready condition
|
||||
ready := "Unknown"
|
||||
status := ""
|
||||
for _, condition := range ps.Status.Conditions {
|
||||
if condition.Type == "Ready" {
|
||||
ready = string(condition.Status)
|
||||
status = condition.Message
|
||||
if len(status) > 48 {
|
||||
status = status[:45] + "..."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", ps.Name, variantsStr, ready, status)
|
||||
|
||||
// Show components if requested
|
||||
if showComponents {
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, component := range variant.Components {
|
||||
fmt.Fprintf(w, " %s\t%s\t\t\n",
|
||||
fmt.Sprintf("%s.%s", ps.Name, component.Name),
|
||||
variant.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listPackages(ctx context.Context, k8sClient client.Client, showComponents bool) error {
|
||||
var pkgList cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &pkgList); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
// Fetch all PackageSource resources once if components are requested
|
||||
var psMap map[string]*cozyv1alpha1.PackageSource
|
||||
if showComponents {
|
||||
var psList cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &psList); err != nil {
|
||||
return fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
psMap = make(map[string]*cozyv1alpha1.PackageSource)
|
||||
for i := range psList.Items {
|
||||
psMap[psList.Items[i].Name] = &psList.Items[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Use tabwriter for better column alignment
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||
defer w.Flush()
|
||||
|
||||
// Print header
|
||||
fmt.Fprintln(w, "NAME\tVARIANT\tREADY\tSTATUS")
|
||||
|
||||
// Print rows
|
||||
for _, pkg := range pkgList.Items {
|
||||
variant := pkg.Spec.Variant
|
||||
if variant == "" {
|
||||
variant = "default"
|
||||
}
|
||||
|
||||
// Get Ready condition
|
||||
ready := "Unknown"
|
||||
status := ""
|
||||
for _, condition := range pkg.Status.Conditions {
|
||||
if condition.Type == "Ready" {
|
||||
ready = string(condition.Status)
|
||||
status = condition.Message
|
||||
if len(status) > 48 {
|
||||
status = status[:45] + "..."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", pkg.Name, variant, ready, status)
|
||||
|
||||
// Show components if requested
|
||||
if showComponents {
|
||||
// Look up PackageSource from map instead of making API call
|
||||
if ps, exists := psMap[pkg.Name]; exists {
|
||||
// Find the variant
|
||||
for _, v := range ps.Spec.Variants {
|
||||
if v.Name == variant {
|
||||
for _, component := range v.Components {
|
||||
fmt.Fprintf(w, " %s\t%s\t\t\n",
|
||||
fmt.Sprintf("%s.%s", pkg.Name, component.Name),
|
||||
variant)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(listCmd)
|
||||
listCmd.Flags().BoolVarP(&listCmdFlags.installed, "installed", "i", false, "list installed Package resources instead of PackageSource resources")
|
||||
listCmd.Flags().BoolVar(&listCmdFlags.components, "components", false, "show components on separate lines")
|
||||
listCmd.Flags().StringVar(&listCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands.
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "cozypkg",
|
||||
Short: "A CLI for managing Cozystack packages",
|
||||
Long: ``,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
DisableAutoGenTag: true,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() error {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Commands are registered in their respective init() functions
|
||||
}
|
||||
|
||||
29
cmd/cozystack-assets-server/main.go
Normal file
29
cmd/cozystack-assets-server/main.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := flag.String("address", ":8123", "Address to listen on")
|
||||
dir := flag.String("dir", "/cozystack/assets", "Directory to serve files from")
|
||||
flag.Parse()
|
||||
|
||||
absDir, err := filepath.Abs(*dir)
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting absolute path for %s: %v", *dir, err)
|
||||
}
|
||||
|
||||
fs := http.FileServer(http.Dir(absDir))
|
||||
http.Handle("/", fs)
|
||||
|
||||
log.Printf("Server starting on %s, serving directory %s", *addr, absDir)
|
||||
|
||||
err = http.ListenAndServe(*addr, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Server failed to start: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,502 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// 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"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"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/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
"github.com/cozystack/cozystack/internal/cozyvaluesreplicator"
|
||||
"github.com/cozystack/cozystack/internal/fluxinstall"
|
||||
"github.com/cozystack/cozystack/internal/operator"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(helmv2.AddToScheme(scheme))
|
||||
utilruntime.Must(sourcev1.AddToScheme(scheme))
|
||||
utilruntime.Must(sourcewatcherv1beta1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var installFlux bool
|
||||
var cozystackVersion string
|
||||
var cozyValuesSecretName string
|
||||
var cozyValuesSecretNamespace string
|
||||
var cozyValuesNamespaceSelector string
|
||||
var platformSourceURL string
|
||||
var platformSourceName string
|
||||
var platformSourceRef string
|
||||
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
|
||||
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", false,
|
||||
"If set the metrics endpoint is served securely")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop")
|
||||
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
|
||||
"Version of Cozystack")
|
||||
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
|
||||
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
|
||||
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
|
||||
flag.StringVar(&cozyValuesSecretName, "cozy-values-secret-name", "cozystack-values", "The name of the secret containing cluster-wide configuration values.")
|
||||
flag.StringVar(&cozyValuesSecretNamespace, "cozy-values-secret-namespace", "cozy-system", "The namespace of the secret containing cluster-wide configuration values.")
|
||||
flag.StringVar(&cozyValuesNamespaceSelector, "cozy-values-namespace-selector", "cozystack.io/system=true", "The label selector for namespaces where the cluster-wide configuration values must be replicated.")
|
||||
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
config := ctrl.GetConfigOrDie()
|
||||
|
||||
// Create a direct client (without cache) for pre-start operations
|
||||
directClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to create direct client")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
targetNSSelector, err := labels.Parse(cozyValuesNamespaceSelector)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "could not parse namespace label selector")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Start the controller manager
|
||||
setupLog.Info("Starting controller manager")
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Cache: cache.Options{
|
||||
ByObject: map[client.Object]cache.ByObject{
|
||||
// Cache only Secrets named <secretName> (in any namespace)
|
||||
&corev1.Secret{}: {
|
||||
Field: fields.OneTermEqualSelector("metadata.name", cozyValuesSecretName),
|
||||
},
|
||||
|
||||
// Cache only Namespaces that match a label selector
|
||||
&corev1.Namespace{}: {
|
||||
Label: targetNSSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
Metrics: metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
},
|
||||
WebhookServer: webhook.NewServer(webhook.Options{
|
||||
Port: 9443,
|
||||
}),
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "cozystack-operator.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, 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)
|
||||
}
|
||||
|
||||
// Install Flux before starting reconcile loop
|
||||
if installFlux {
|
||||
setupLog.Info("Installing Flux components before starting reconcile loop")
|
||||
installCtx, installCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer installCancel()
|
||||
|
||||
// Use direct client for pre-start operations (cache is not ready yet)
|
||||
if err := fluxinstall.Install(installCtx, directClient, fluxinstall.WriteEmbeddedManifests); err != nil {
|
||||
setupLog.Error(err, "failed to install Flux")
|
||||
os.Exit(1)
|
||||
}
|
||||
setupLog.Info("Flux installation completed successfully")
|
||||
}
|
||||
|
||||
// Generate and install platform source resource if specified
|
||||
if platformSourceURL != "" {
|
||||
setupLog.Info("Generating platform source resource", "url", platformSourceURL, "name", platformSourceName, "ref", platformSourceRef)
|
||||
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer installCancel()
|
||||
|
||||
// Use direct client for pre-start operations (cache is not ready yet)
|
||||
if err := installPlatformSourceResource(installCtx, directClient, platformSourceURL, platformSourceName, platformSourceRef); err != nil {
|
||||
setupLog.Error(err, "failed to install platform source resource")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
setupLog.Info("Platform source resource installation completed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// Setup PackageSource reconciler
|
||||
if err := (&operator.PackageSourceReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "PackageSource")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Setup Package reconciler
|
||||
if err := (&operator.PackageReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Package")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Setup CozyValuesReplicator reconciler
|
||||
if err := (&cozyvaluesreplicator.SecretReplicatorReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
SourceNamespace: cozyValuesSecretNamespace,
|
||||
SecretName: cozyValuesSecretName,
|
||||
TargetNamespaceSelector: targetNSSelector,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozyValuesReplicator")
|
||||
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 controller manager")
|
||||
mgrCtx := ctrl.SetupSignalHandler()
|
||||
if err := mgr.Start(mgrCtx); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// installPlatformSourceResource generates and installs a Flux source resource (OCIRepository or GitRepository)
|
||||
// based on the platform source URL
|
||||
func installPlatformSourceResource(ctx context.Context, k8sClient client.Client, sourceURL, resourceName, refSpec string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Parse the source URL to determine type
|
||||
sourceType, repoURL, err := parsePlatformSourceURL(sourceURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse platform source URL: %w", err)
|
||||
}
|
||||
|
||||
// Parse reference specification
|
||||
refMap, err := parseRefSpec(refSpec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse reference specification: %w", err)
|
||||
}
|
||||
|
||||
var obj client.Object
|
||||
switch sourceType {
|
||||
case "oci":
|
||||
obj, err = generateOCIRepository(resourceName, repoURL, refMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate OCIRepository: %w", err)
|
||||
}
|
||||
case "git":
|
||||
obj, err = generateGitRepository(resourceName, repoURL, refMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate GitRepository: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported source type: %s (expected oci:// or https://)", sourceType)
|
||||
}
|
||||
|
||||
// Apply the resource (create or update)
|
||||
logger.Info("Applying platform source resource",
|
||||
"apiVersion", obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
||||
"kind", obj.GetObjectKind().GroupVersionKind().Kind,
|
||||
"name", obj.GetName(),
|
||||
"namespace", obj.GetNamespace(),
|
||||
)
|
||||
|
||||
existing := obj.DeepCopyObject().(client.Object)
|
||||
key := client.ObjectKeyFromObject(obj)
|
||||
|
||||
err = k8sClient.Get(ctx, key, existing)
|
||||
if err != nil {
|
||||
if client.IgnoreNotFound(err) == nil {
|
||||
// Resource doesn't exist, create it
|
||||
if err := k8sClient.Create(ctx, obj); err != nil {
|
||||
return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Created platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
|
||||
} else {
|
||||
return fmt.Errorf("failed to check if resource exists: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Resource exists, update it
|
||||
obj.SetResourceVersion(existing.GetResourceVersion())
|
||||
if err := k8sClient.Update(ctx, obj); err != nil {
|
||||
return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Updated platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePlatformSourceURL parses the source URL and returns the source type and repository URL.
|
||||
// Supports formats:
|
||||
// - oci://registry.example.com/repo
|
||||
// - https://github.com/user/repo
|
||||
// - http://github.com/user/repo
|
||||
// - ssh://git@github.com/user/repo
|
||||
func parsePlatformSourceURL(sourceURL string) (sourceType, repoURL string, err error) {
|
||||
sourceURL = strings.TrimSpace(sourceURL)
|
||||
|
||||
if strings.HasPrefix(sourceURL, "oci://") {
|
||||
return "oci", sourceURL, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(sourceURL, "https://") || strings.HasPrefix(sourceURL, "http://") || strings.HasPrefix(sourceURL, "ssh://") {
|
||||
return "git", sourceURL, nil
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("unsupported source URL scheme (expected oci://, https://, http://, or ssh://): %s", sourceURL)
|
||||
}
|
||||
|
||||
// parseRefSpec parses a reference specification string in the format "key1=value1,key2=value2".
|
||||
// Returns a map of key-value pairs.
|
||||
func parseRefSpec(refSpec string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
refSpec = strings.TrimSpace(refSpec)
|
||||
if refSpec == "" {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
pairs := strings.Split(refSpec, ",")
|
||||
for _, pair := range pairs {
|
||||
pair = strings.TrimSpace(pair)
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split on first '=' only to allow '=' in values (e.g., digest=sha256:...)
|
||||
idx := strings.Index(pair, "=")
|
||||
if idx == -1 {
|
||||
return nil, fmt.Errorf("invalid reference specification format: %q (expected key=value)", pair)
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(pair[:idx])
|
||||
value := strings.TrimSpace(pair[idx+1:])
|
||||
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("empty key in reference specification: %q", pair)
|
||||
}
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("empty value for key %q in reference specification", key)
|
||||
}
|
||||
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Valid reference keys for OCI repositories
|
||||
var validOCIRefKeys = map[string]bool{
|
||||
"digest": true,
|
||||
"semver": true,
|
||||
"semverFilter": true,
|
||||
"tag": true,
|
||||
}
|
||||
|
||||
// Valid reference keys for Git repositories
|
||||
var validGitRefKeys = map[string]bool{
|
||||
"branch": true,
|
||||
"tag": true,
|
||||
"semver": true,
|
||||
"name": true,
|
||||
"commit": true,
|
||||
}
|
||||
|
||||
// validateOCIRef validates reference keys for OCI repositories
|
||||
func validateOCIRef(refMap map[string]string) error {
|
||||
for key := range refMap {
|
||||
if !validOCIRefKeys[key] {
|
||||
return fmt.Errorf("invalid OCI reference key %q (valid keys: digest, semver, semverFilter, tag)", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate digest format if provided
|
||||
if digest, ok := refMap["digest"]; ok {
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
return fmt.Errorf("digest must be in format 'sha256:<hash>', got: %s", digest)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateGitRef validates reference keys for Git repositories
|
||||
func validateGitRef(refMap map[string]string) error {
|
||||
for key := range refMap {
|
||||
if !validGitRefKeys[key] {
|
||||
return fmt.Errorf("invalid Git reference key %q (valid keys: branch, tag, semver, name, commit)", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate commit format if provided (should be a hex string)
|
||||
if commit, ok := refMap["commit"]; ok {
|
||||
if len(commit) < 7 {
|
||||
return fmt.Errorf("commit SHA should be at least 7 characters, got: %s", commit)
|
||||
}
|
||||
for _, c := range commit {
|
||||
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
|
||||
return fmt.Errorf("commit SHA should be a hexadecimal string, got: %s", commit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateOCIRepository creates an OCIRepository resource
|
||||
func generateOCIRepository(name, repoURL string, refMap map[string]string) (*sourcev1.OCIRepository, error) {
|
||||
if err := validateOCIRef(refMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := &sourcev1.OCIRepository{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: sourcev1.GroupVersion.String(),
|
||||
Kind: sourcev1.OCIRepositoryKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: "cozy-system",
|
||||
},
|
||||
Spec: sourcev1.OCIRepositorySpec{
|
||||
URL: repoURL,
|
||||
Interval: metav1.Duration{Duration: 5 * time.Minute},
|
||||
},
|
||||
}
|
||||
|
||||
// Set reference if any ref options are provided
|
||||
if len(refMap) > 0 {
|
||||
obj.Spec.Reference = &sourcev1.OCIRepositoryRef{
|
||||
Digest: refMap["digest"],
|
||||
SemVer: refMap["semver"],
|
||||
SemverFilter: refMap["semverFilter"],
|
||||
Tag: refMap["tag"],
|
||||
}
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// generateGitRepository creates a GitRepository resource
|
||||
func generateGitRepository(name, repoURL string, refMap map[string]string) (*sourcev1.GitRepository, error) {
|
||||
if err := validateGitRef(refMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := &sourcev1.GitRepository{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: sourcev1.GroupVersion.String(),
|
||||
Kind: sourcev1.GitRepositoryKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: "cozy-system",
|
||||
},
|
||||
Spec: sourcev1.GitRepositorySpec{
|
||||
URL: repoURL,
|
||||
Interval: metav1.Duration{Duration: 5 * time.Minute},
|
||||
},
|
||||
}
|
||||
|
||||
// Set reference if any ref options are provided
|
||||
if len(refMap) > 0 {
|
||||
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
|
||||
Branch: refMap["branch"],
|
||||
Tag: refMap["tag"],
|
||||
SemVer: refMap["semver"],
|
||||
Name: refMap["name"],
|
||||
Commit: refMap["commit"],
|
||||
}
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
39
go.mod
39
go.mod
@@ -5,10 +5,7 @@ module github.com/cozystack/cozystack
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/emicklei/dot v1.10.0
|
||||
github.com/fluxcd/helm-controller/api v1.4.3
|
||||
github.com/fluxcd/source-controller/api v1.7.4
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/gofuzz v1.2.0
|
||||
@@ -17,19 +14,18 @@ require (
|
||||
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/vmware-tanzu/velero v1.17.1
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.2
|
||||
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-20250710124328-f3f2b991d03b
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
sigs.k8s.io/controller-runtime v0.22.2
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
|
||||
)
|
||||
|
||||
@@ -48,9 +44,8 @@ require (
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.13.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/meta v1.23.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/meta v1.22.0 // 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
|
||||
@@ -81,8 +76,8 @@ require (
|
||||
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_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
@@ -91,14 +86,14 @@ require (
|
||||
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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.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/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
@@ -106,18 +101,18 @@ require (
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/oauth2 v0.29.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // 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
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // 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
|
||||
|
||||
78
go.sum
78
go.sum
@@ -27,8 +27,6 @@ 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/dot v1.10.0 h1:z17n0ce/FBMz3QbShSzVGhiW447Qhu7fljzvp3Gs6ig=
|
||||
github.com/emicklei/dot v1.10.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
|
||||
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 v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
@@ -39,16 +37,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fluxcd/helm-controller/api v1.4.3 h1:CdZwjL1liXmYCWyk2jscmFEB59tICIlnWB9PfDDW5q4=
|
||||
github.com/fluxcd/helm-controller/api v1.4.3/go.mod h1:0XrBhKEaqvxyDj/FziG1Q8Fmx2UATdaqLgYqmZh6wW4=
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.13.0 h1:GGf0UBVRIku+gebY944icVeEIhyg1P/KE3IrhOyJJnE=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.13.0/go.mod h1:TLKVqbtnzkhDuhWnAsN35977HvRfIjs+lgMuNro/LEc=
|
||||
github.com/fluxcd/pkg/apis/meta v1.23.0 h1:fLis5YcHnOsyKYptzBtituBm5EWNx13I0bXQsy0FG4s=
|
||||
github.com/fluxcd/pkg/apis/meta v1.23.0/go.mod h1:UWsIbBPCxYvoVklr2mV2uLFBf/n17dNAmKFjRfApdDo=
|
||||
github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs=
|
||||
github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3 h1:SsVGAaMBxzvcgrOz/Kl6c2ybMHVqoiEFwtI+bDuSeSs=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3/go.mod h1:Nx3QZweVyuhaOtSNrw+oxifG+qrakPvjgNAN9qlUTb0=
|
||||
github.com/fluxcd/pkg/apis/meta v1.22.0 h1:EHWQH5ZWml7i8eZ/AMjm1jxid3j/PQ31p+hIwCt6crM=
|
||||
github.com/fluxcd/pkg/apis/meta v1.22.0/go.mod h1:Kc1+bWe5p0doROzuV9XiTfV/oL3ddsemYXt8ZYWdVVg=
|
||||
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=
|
||||
@@ -144,10 +136,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
@@ -179,8 +171,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=
|
||||
@@ -203,24 +193,24 @@ 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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
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/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
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=
|
||||
@@ -248,8 +238,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
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/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
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=
|
||||
@@ -266,8 +256,8 @@ 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.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.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=
|
||||
@@ -280,14 +270,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
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/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=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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=
|
||||
@@ -322,8 +312,8 @@ k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPG
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/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/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4=
|
||||
sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
#!/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" ]
|
||||
}
|
||||
|
||||
31
hack/e2e-apps/mongodb.bats
Normal file
31
hack/e2e-apps/mongodb.bats
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
@test "Create DB MongoDB" {
|
||||
name='test'
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: MongoDB
|
||||
metadata:
|
||||
name: $name
|
||||
namespace: tenant-test
|
||||
spec:
|
||||
external: false
|
||||
size: 10Gi
|
||||
replicas: 1
|
||||
storageClass: ""
|
||||
resourcesPreset: "nano"
|
||||
backup:
|
||||
enabled: false
|
||||
EOF
|
||||
sleep 5
|
||||
# Wait for HelmRelease
|
||||
kubectl -n tenant-test wait hr mongodb-$name --timeout=60s --for=condition=ready
|
||||
# Wait for MongoDB service (port 27017)
|
||||
timeout 120 sh -ec "until kubectl -n tenant-test get svc mongodb-$name-rs0 -o jsonpath='{.spec.ports[0].port}' | grep -q '27017'; do sleep 10; done"
|
||||
# Wait for endpoints
|
||||
timeout 180 sh -ec "until kubectl -n tenant-test get endpoints mongodb-$name-rs0 -o jsonpath='{.subsets[*].addresses[*].ip}' | grep -q '[0-9]'; do sleep 10; done"
|
||||
# Wait for StatefulSet replicas
|
||||
kubectl -n tenant-test wait statefulset.apps/mongodb-$name-rs0 --timeout=300s --for=jsonpath='{.status.replicas}'=1
|
||||
# Cleanup
|
||||
kubectl -n tenant-test delete mongodbs.apps.cozystack.io $name
|
||||
}
|
||||
@@ -23,14 +23,6 @@ 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)
|
||||
OPERATOR_CRDDIR=packages/core/installer/definitions
|
||||
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions
|
||||
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"
|
||||
|
||||
@@ -61,15 +53,6 @@ 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=${TMPDIR}
|
||||
|
||||
mv ${TMPDIR}/cozystack.io_packages.yaml ${OPERATOR_CRDDIR}/cozystack.io_packages.yaml
|
||||
mv ${TMPDIR}/cozystack.io_packagesources.yaml ${OPERATOR_CRDDIR}/cozystack.io_packagesources.yaml
|
||||
|
||||
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}/
|
||||
$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
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"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 {
|
||||
logger.V(1).Info("BackupJob has nil StrategyRef.APIGroup, skipping", "backupjob", j.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if *j.Spec.StrategyRef.APIGroup != strategyv1alpha1.GroupVersion.Group {
|
||||
logger.V(1).Info("BackupJob StrategyRef.APIGroup doesn't match, skipping",
|
||||
"backupjob", j.Name,
|
||||
"expected", strategyv1alpha1.GroupVersion.Group,
|
||||
"got", *j.Spec.StrategyRef.APIGroup)
|
||||
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:
|
||||
logger.V(1).Info("BackupJob StrategyRef.Kind not supported, skipping",
|
||||
"backupjob", j.Name,
|
||||
"kind", j.Spec.StrategyRef.Kind,
|
||||
"supported", []string{strategyv1alpha1.JobStrategyKind, strategyv1alpha1.VeleroStrategyKind})
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,627 +0,0 @@
|
||||
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
|
||||
// Velero requires API objects and secrets to be in the cozy-velero namespace
|
||||
veleroNamespace = "cozy-velero"
|
||||
virtualMachinePrefix = "virtual-machine-"
|
||||
)
|
||||
|
||||
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
|
||||
// Use human-readable timestamp: YYYY-MM-DD-HH-MM-SS
|
||||
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) 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
|
||||
}
|
||||
|
||||
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{
|
||||
"velero.io/backup-name": veleroBackup.Name,
|
||||
"velero.io/backup-namespace": 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
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleasesForCRD(ctx
|
||||
labelSelector := client.MatchingLabels{
|
||||
"apps.cozystack.io/application.kind": applicationKind,
|
||||
"apps.cozystack.io/application.group": applicationGroup,
|
||||
"cozystack.io/ui": "true",
|
||||
"cozystack.io/ui": "true",
|
||||
}
|
||||
|
||||
// List all HelmReleases with matching labels
|
||||
@@ -130,30 +130,55 @@ func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleaseChart(ctx c
|
||||
hrCopy := hr.DeepCopy()
|
||||
updated := false
|
||||
|
||||
// Validate ChartRef configuration exists
|
||||
if crd.Spec.Release.ChartRef == nil ||
|
||||
crd.Spec.Release.ChartRef.Kind == "" ||
|
||||
crd.Spec.Release.ChartRef.Name == "" ||
|
||||
crd.Spec.Release.ChartRef.Namespace == "" {
|
||||
logger.Error(fmt.Errorf("invalid ChartRef in CRD"), "Skipping HelmRelease chartRef update: ChartRef is nil or incomplete",
|
||||
"crd", crd.Name)
|
||||
// Validate Chart configuration exists
|
||||
if crd.Spec.Release.Chart.Name == "" {
|
||||
logger.V(4).Info("Skipping HelmRelease chart update: Chart.Name is empty", "crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use ChartRef directly from CRD
|
||||
expectedChartRef := crd.Spec.Release.ChartRef
|
||||
// Validate SourceRef fields
|
||||
if crd.Spec.Release.Chart.SourceRef.Kind == "" ||
|
||||
crd.Spec.Release.Chart.SourceRef.Name == "" ||
|
||||
crd.Spec.Release.Chart.SourceRef.Namespace == "" {
|
||||
logger.Error(fmt.Errorf("invalid SourceRef in CRD"), "Skipping HelmRelease chart update: SourceRef fields are incomplete",
|
||||
"crd", crd.Name,
|
||||
"kind", crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
"name", crd.Spec.Release.Chart.SourceRef.Name,
|
||||
"namespace", crd.Spec.Release.Chart.SourceRef.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if chartRef needs to be updated
|
||||
if hrCopy.Spec.ChartRef == nil {
|
||||
hrCopy.Spec.ChartRef = expectedChartRef
|
||||
// Clear the old chart field when switching to chartRef
|
||||
hrCopy.Spec.Chart = nil
|
||||
updated = true
|
||||
} else if hrCopy.Spec.ChartRef.Kind != expectedChartRef.Kind ||
|
||||
hrCopy.Spec.ChartRef.Name != expectedChartRef.Name ||
|
||||
hrCopy.Spec.ChartRef.Namespace != expectedChartRef.Namespace {
|
||||
hrCopy.Spec.ChartRef = expectedChartRef
|
||||
// Get version and reconcileStrategy from CRD or use defaults
|
||||
version := ">= 0.0.0-0"
|
||||
reconcileStrategy := "Revision"
|
||||
// TODO: Add Version and ReconcileStrategy fields to CozystackResourceDefinitionChart if needed
|
||||
|
||||
// Build expected SourceRef
|
||||
expectedSourceRef := helmv2.CrossNamespaceObjectReference{
|
||||
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
Name: crd.Spec.Release.Chart.SourceRef.Name,
|
||||
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
|
||||
}
|
||||
|
||||
if hrCopy.Spec.Chart == nil {
|
||||
// Need to create Chart spec
|
||||
hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{
|
||||
Spec: helmv2.HelmChartTemplateSpec{
|
||||
Chart: crd.Spec.Release.Chart.Name,
|
||||
Version: version,
|
||||
ReconcileStrategy: reconcileStrategy,
|
||||
SourceRef: expectedSourceRef,
|
||||
},
|
||||
}
|
||||
updated = true
|
||||
} else {
|
||||
// Update existing Chart spec
|
||||
if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name ||
|
||||
hrCopy.Spec.Chart.Spec.SourceRef != expectedSourceRef {
|
||||
hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name
|
||||
hrCopy.Spec.Chart.Spec.SourceRef = expectedSourceRef
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check and update valuesFrom configuration
|
||||
@@ -165,7 +190,7 @@ func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleaseChart(ctx c
|
||||
}
|
||||
|
||||
if updated {
|
||||
logger.V(4).Info("Updating HelmRelease chartRef", "name", hr.Name, "namespace", hr.Namespace)
|
||||
logger.V(4).Info("Updating HelmRelease chart", "name", hr.Name, "namespace", hr.Namespace)
|
||||
if err := r.Update(ctx, hrCopy); err != nil {
|
||||
return fmt.Errorf("failed to update HelmRelease: %w", err)
|
||||
}
|
||||
@@ -173,3 +198,4 @@ func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleaseChart(ctx c
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid
|
||||
createCustomColumnsOverride("factory-details-v1.services", []any{
|
||||
createCustomColumnWithSpecificColor("Name", "Service", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
|
||||
createStringColumn("ClusterIP", ".spec.clusterIP"),
|
||||
createStringColumn("LoadbalancerIP", ".spec.loadBalancerIP"),
|
||||
createStringColumn("LoadbalancerIP", ".status.loadBalancer.ingress[0].ip"),
|
||||
createTimestampColumn("Created", ".metadata.creationTimestamp"),
|
||||
}),
|
||||
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package cozyvaluesreplicator
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// SecretReplicatorReconciler replicates a source secret to namespaces matching a label selector.
|
||||
type SecretReplicatorReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
// Source of truth:
|
||||
SourceNamespace string
|
||||
SecretName string
|
||||
|
||||
// Namespaces to replicate into:
|
||||
// (e.g. labels.SelectorFromSet(labels.Set{"tenant":"true"}), or metav1.LabelSelectorAsSelector(...))
|
||||
TargetNamespaceSelector labels.Selector
|
||||
}
|
||||
|
||||
func (r *SecretReplicatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
// 1) Primary watch for requirement (b):
|
||||
// Reconcile any Secret named r.SecretName in any namespace (includes source too).
|
||||
// This keeps Secrets in cache and causes "copy changed -> reconcile it" to happen.
|
||||
secretNameOnly := predicate.NewPredicateFuncs(func(obj client.Object) bool {
|
||||
return obj.GetName() == r.SecretName
|
||||
})
|
||||
|
||||
// 2) Secondary watch for requirement (c):
|
||||
// When the *source* Secret changes, fan-out reconcile requests to every matching namespace.
|
||||
onlySourceSecret := predicate.Funcs{
|
||||
CreateFunc: func(e event.CreateEvent) bool { return isSourceSecret(e.Object, r) },
|
||||
UpdateFunc: func(e event.UpdateEvent) bool { return isSourceSecret(e.ObjectNew, r) },
|
||||
DeleteFunc: func(e event.DeleteEvent) bool { return isSourceSecret(e.Object, r) },
|
||||
GenericFunc: func(e event.GenericEvent) bool {
|
||||
return isSourceSecret(e.Object, r)
|
||||
},
|
||||
}
|
||||
|
||||
// Fan-out mapper for source Secret events -> one request per matching target namespace.
|
||||
fanOutOnSourceSecret := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, _ client.Object) []reconcile.Request {
|
||||
// List namespaces *from the cache* (because we also watch Namespaces below).
|
||||
var nsList corev1.NamespaceList
|
||||
if err := r.List(ctx, &nsList); err != nil {
|
||||
// If list fails, best-effort: return nothing; reconcile will be retried by next event.
|
||||
return nil
|
||||
}
|
||||
|
||||
reqs := make([]reconcile.Request, 0, len(nsList.Items))
|
||||
for i := range nsList.Items {
|
||||
ns := &nsList.Items[i]
|
||||
if ns.Name == r.SourceNamespace {
|
||||
continue
|
||||
}
|
||||
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
|
||||
continue
|
||||
}
|
||||
reqs = append(reqs, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: ns.Name,
|
||||
Name: r.SecretName,
|
||||
},
|
||||
})
|
||||
}
|
||||
return reqs
|
||||
})
|
||||
|
||||
// 3) Namespace watch for requirement (a):
|
||||
// When a namespace is created/updated to match selector, enqueue reconcile for the Secret copy in that namespace.
|
||||
enqueueOnNamespaceMatch := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
ns, ok := obj.(*corev1.Namespace)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if ns.Name == r.SourceNamespace {
|
||||
return nil
|
||||
}
|
||||
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
|
||||
return nil
|
||||
}
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: ns.Name,
|
||||
Name: r.SecretName,
|
||||
},
|
||||
}}
|
||||
})
|
||||
|
||||
// Only trigger from namespace events where the label match may be (or become) true.
|
||||
// (You can keep this simple; it's fine if it fires on any update—your Reconcile should be idempotent.)
|
||||
namespaceMayMatter := predicate.Funcs{
|
||||
CreateFunc: func(e event.CreateEvent) bool {
|
||||
ns, ok := e.Object.(*corev1.Namespace)
|
||||
return ok && (r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)))
|
||||
},
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
oldNS, okOld := e.ObjectOld.(*corev1.Namespace)
|
||||
newNS, okNew := e.ObjectNew.(*corev1.Namespace)
|
||||
if !okOld || !okNew {
|
||||
return false
|
||||
}
|
||||
// Fire if it matches now OR matched before (covers transitions both ways; reconcile can decide what to do).
|
||||
oldMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(oldNS.Labels))
|
||||
newMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(newNS.Labels))
|
||||
return oldMatch || newMatch
|
||||
},
|
||||
DeleteFunc: func(event.DeleteEvent) bool { return false }, // nothing to do on namespace delete
|
||||
GenericFunc: func(event.GenericEvent) bool { return false },
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
// (b) Watch all Secrets with the chosen name; this also ensures Secret objects are cached.
|
||||
For(&corev1.Secret{}, builder.WithPredicates(secretNameOnly)).
|
||||
|
||||
// (c) Add a second watch on Secret, but only for the source secret, and fan-out to all namespaces.
|
||||
Watches(
|
||||
&corev1.Secret{},
|
||||
fanOutOnSourceSecret,
|
||||
builder.WithPredicates(onlySourceSecret),
|
||||
).
|
||||
|
||||
// (a) Watch Namespaces so they're cached and so "namespace appears / starts matching" enqueues reconcile.
|
||||
Watches(
|
||||
&corev1.Namespace{},
|
||||
enqueueOnNamespaceMatch,
|
||||
builder.WithPredicates(namespaceMayMatter),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func isSourceSecret(obj client.Object, r *SecretReplicatorReconciler) bool {
|
||||
if obj == nil {
|
||||
return false
|
||||
}
|
||||
return obj.GetNamespace() == r.SourceNamespace && obj.GetName() == r.SecretName
|
||||
}
|
||||
|
||||
func (r *SecretReplicatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Ignore requests that don't match our secret name or are for the source namespace
|
||||
if req.Name != r.SecretName || req.Namespace == r.SourceNamespace {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Verify the target namespace still exists and matches the selector
|
||||
targetNamespace := &corev1.Namespace{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: req.Namespace}, targetNamespace); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Namespace doesn't exist, nothing to do
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Error(err, "Failed to get target namespace", "namespace", req.Namespace)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Check if namespace still matches the selector
|
||||
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(targetNamespace.Labels)) {
|
||||
// Namespace no longer matches selector, delete the replicated secret if it exists
|
||||
replicatedSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: req.Namespace,
|
||||
Name: req.Name,
|
||||
},
|
||||
}
|
||||
if err := r.Delete(ctx, replicatedSecret); err != nil && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "Failed to delete replicated secret from non-matching namespace",
|
||||
"namespace", req.Namespace, "secret", req.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Get the source secret
|
||||
originalSecret := &corev1.Secret{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: r.SourceNamespace, Name: r.SecretName}, originalSecret); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Source secret doesn't exist, delete the replicated secret if it exists
|
||||
replicatedSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: req.Namespace,
|
||||
Name: req.Name,
|
||||
},
|
||||
}
|
||||
if err := r.Delete(ctx, replicatedSecret); err != nil && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "Failed to delete replicated secret after source secret deletion",
|
||||
"namespace", req.Namespace, "secret", req.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Error(err, "Failed to get source secret",
|
||||
"namespace", r.SourceNamespace, "secret", r.SecretName)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Create or update the replicated secret
|
||||
replicatedSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: req.Namespace,
|
||||
Name: req.Name,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, replicatedSecret, func() error {
|
||||
// Copy the secret data and type from the source
|
||||
replicatedSecret.Data = make(map[string][]byte)
|
||||
for k, v := range originalSecret.Data {
|
||||
replicatedSecret.Data[k] = v
|
||||
}
|
||||
replicatedSecret.Type = originalSecret.Type
|
||||
|
||||
// Copy labels and annotations from source (if any)
|
||||
if originalSecret.Labels != nil {
|
||||
if replicatedSecret.Labels == nil {
|
||||
replicatedSecret.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range originalSecret.Labels {
|
||||
replicatedSecret.Labels[k] = v
|
||||
}
|
||||
}
|
||||
if originalSecret.Annotations != nil {
|
||||
if replicatedSecret.Annotations == nil {
|
||||
replicatedSecret.Annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range originalSecret.Annotations {
|
||||
replicatedSecret.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to create or update replicated secret",
|
||||
"namespace", req.Namespace, "secret", req.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package fluxinstall
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// Install installs Flux components using embedded manifests.
|
||||
// It extracts the manifests and applies them to the cluster.
|
||||
// The namespace is automatically determined from the Namespace object in the manifests.
|
||||
func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifests func(string) error) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Create temporary directory for manifests
|
||||
tmpDir, err := os.MkdirTemp("", "flux-install-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Extract embedded manifests (generated by cozypkg)
|
||||
manifestsDir := filepath.Join(tmpDir, "manifests")
|
||||
if err := os.MkdirAll(manifestsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create manifests directory: %w", err)
|
||||
}
|
||||
|
||||
if err := writeEmbeddedManifests(manifestsDir); err != nil {
|
||||
return fmt.Errorf("failed to extract embedded manifests: %w", err)
|
||||
}
|
||||
|
||||
// Find the manifest file (should be fluxcd.yaml from cozypkg)
|
||||
manifestPath := filepath.Join(manifestsDir, "fluxcd.yaml")
|
||||
if _, err := os.Stat(manifestPath); err != nil {
|
||||
// Try to find any YAML file if fluxcd.yaml doesn't exist
|
||||
entries, err := os.ReadDir(manifestsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read manifests directory: %w", err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry.Name(), ".yaml") {
|
||||
manifestPath = filepath.Join(manifestsDir, entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and apply manifests
|
||||
objects, err := parseManifests(manifestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse manifests: %w", err)
|
||||
}
|
||||
|
||||
if len(objects) == 0 {
|
||||
return fmt.Errorf("no objects found in manifests")
|
||||
}
|
||||
|
||||
// Inject KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT if set in operator environment
|
||||
if err := injectKubernetesServiceEnv(objects); err != nil {
|
||||
logger.Info("Failed to inject KUBERNETES_SERVICE_* env vars, continuing anyway", "error", err)
|
||||
}
|
||||
|
||||
// Extract namespace from Namespace object in manifests
|
||||
namespace, err := extractNamespace(objects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract namespace from manifests: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Installing Flux components", "namespace", namespace)
|
||||
|
||||
// Apply manifests using server-side apply
|
||||
logger.Info("Applying Flux manifests", "count", len(objects), "manifest", manifestPath, "namespace", namespace)
|
||||
if err := applyManifests(ctx, k8sClient, objects); err != nil {
|
||||
return fmt.Errorf("failed to apply manifests: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Flux installation completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseManifests parses YAML manifests into unstructured objects.
|
||||
func parseManifests(manifestPath string) ([]*unstructured.Unstructured, error) {
|
||||
data, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read manifest file: %w", err)
|
||||
}
|
||||
|
||||
return readYAMLObjects(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// readYAMLObjects parses multi-document YAML into unstructured objects.
|
||||
func readYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) {
|
||||
var objects []*unstructured.Unstructured
|
||||
yamlReader := k8syaml.NewYAMLReader(bufio.NewReader(reader))
|
||||
|
||||
for {
|
||||
doc, err := yamlReader.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read YAML document: %w", err)
|
||||
}
|
||||
|
||||
// Skip empty documents
|
||||
if len(bytes.TrimSpace(doc)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
obj := &unstructured.Unstructured{}
|
||||
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(doc), len(doc))
|
||||
if err := decoder.Decode(obj); err != nil {
|
||||
// Skip documents that can't be decoded (might be comments or empty)
|
||||
if err == io.EOF {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("failed to decode YAML document: %w", err)
|
||||
}
|
||||
|
||||
// Skip empty objects (no kind)
|
||||
if obj.GetKind() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
// applyManifests applies Kubernetes objects using server-side apply.
|
||||
func applyManifests(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Separate CRDs and namespaces from other resources
|
||||
var stageOne []*unstructured.Unstructured // CRDs and Namespaces
|
||||
var stageTwo []*unstructured.Unstructured // Everything else
|
||||
|
||||
for _, obj := range objects {
|
||||
if isClusterDefinition(obj) {
|
||||
stageOne = append(stageOne, obj)
|
||||
} else {
|
||||
stageTwo = append(stageTwo, obj)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply stage one (CRDs and Namespaces) first
|
||||
if len(stageOne) > 0 {
|
||||
logger.Info("Applying cluster definitions", "count", len(stageOne))
|
||||
if err := applyObjects(ctx, k8sClient, stageOne); err != nil {
|
||||
return fmt.Errorf("failed to apply cluster definitions: %w", err)
|
||||
}
|
||||
|
||||
// Wait a bit for CRDs to be registered
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
// Apply stage two (everything else)
|
||||
if len(stageTwo) > 0 {
|
||||
logger.Info("Applying resources", "count", len(stageTwo))
|
||||
if err := applyObjects(ctx, k8sClient, stageTwo); err != nil {
|
||||
return fmt.Errorf("failed to apply resources: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyObjects applies a list of objects using server-side apply.
|
||||
func applyObjects(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error {
|
||||
for _, obj := range objects {
|
||||
// Use server-side apply with force ownership and field manager
|
||||
// FieldManager is required for apply patch operations
|
||||
patchOptions := &client.PatchOptions{
|
||||
FieldManager: "cozystack-operator",
|
||||
Force: func() *bool { b := true; return &b }(),
|
||||
}
|
||||
|
||||
if err := k8sClient.Patch(ctx, obj, client.Apply, patchOptions); err != nil {
|
||||
return fmt.Errorf("failed to apply object %s/%s: %w", obj.GetKind(), obj.GetName(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// extractNamespace extracts the namespace name from the Namespace object in the manifests.
|
||||
func extractNamespace(objects []*unstructured.Unstructured) (string, error) {
|
||||
for _, obj := range objects {
|
||||
if obj.GetKind() == "Namespace" {
|
||||
namespace := obj.GetName()
|
||||
if namespace == "" {
|
||||
return "", fmt.Errorf("Namespace object has no name")
|
||||
}
|
||||
return namespace, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no Namespace object found in manifests")
|
||||
}
|
||||
|
||||
// isClusterDefinition checks if an object is a CRD or Namespace.
|
||||
func isClusterDefinition(obj *unstructured.Unstructured) bool {
|
||||
kind := obj.GetKind()
|
||||
return kind == "CustomResourceDefinition" || kind == "Namespace"
|
||||
}
|
||||
|
||||
// injectKubernetesServiceEnv injects KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT
|
||||
// environment variables into all containers of Deployment, StatefulSet, and DaemonSet objects
|
||||
// if these variables are set in the operator's environment.
|
||||
// Errors are logged but do not stop processing of other objects.
|
||||
func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
|
||||
kubernetesHost := os.Getenv("KUBERNETES_SERVICE_HOST")
|
||||
kubernetesPort := os.Getenv("KUBERNETES_SERVICE_PORT")
|
||||
|
||||
// If neither variable is set, nothing to do
|
||||
if kubernetesHost == "" && kubernetesPort == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for _, obj := range objects {
|
||||
kind := obj.GetKind()
|
||||
if kind != "Deployment" && kind != "StatefulSet" && kind != "DaemonSet" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Navigate to spec.template.spec.containers
|
||||
spec, found, err := unstructured.NestedMap(obj.Object, "spec", "template", "spec")
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to get spec for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Update containers
|
||||
containers, found, err := unstructured.NestedSlice(spec, "containers")
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to get containers for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if found {
|
||||
containers = updateContainersEnv(containers, kubernetesHost, kubernetesPort)
|
||||
if err := unstructured.SetNestedSlice(spec, containers, "containers"); err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to set containers for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update initContainers
|
||||
initContainers, found, err := unstructured.NestedSlice(spec, "initContainers")
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to get initContainers for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if found {
|
||||
initContainers = updateContainersEnv(initContainers, kubernetesHost, kubernetesPort)
|
||||
if err := unstructured.SetNestedSlice(spec, initContainers, "initContainers"); err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to set initContainers for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update spec in the object
|
||||
if err := unstructured.SetNestedMap(obj.Object, spec, "spec", "template", "spec"); err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to update spec for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// updateContainersEnv updates environment variables for a slice of containers.
|
||||
func updateContainersEnv(containers []interface{}, kubernetesHost, kubernetesPort string) []interface{} {
|
||||
for i, container := range containers {
|
||||
containerMap, ok := container.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
env, found, err := unstructured.NestedSlice(containerMap, "env")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !found {
|
||||
env = []interface{}{}
|
||||
}
|
||||
|
||||
// Update or add KUBERNETES_SERVICE_HOST
|
||||
if kubernetesHost != "" {
|
||||
env = setEnvVar(env, "KUBERNETES_SERVICE_HOST", kubernetesHost)
|
||||
}
|
||||
|
||||
// Update or add KUBERNETES_SERVICE_PORT
|
||||
if kubernetesPort != "" {
|
||||
env = setEnvVar(env, "KUBERNETES_SERVICE_PORT", kubernetesPort)
|
||||
}
|
||||
|
||||
// Update the container's env
|
||||
if err := unstructured.SetNestedSlice(containerMap, env, "env"); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update the container in the slice
|
||||
containers[i] = containerMap
|
||||
}
|
||||
|
||||
return containers
|
||||
}
|
||||
|
||||
// setEnvVar updates or adds an environment variable in the env slice.
|
||||
func setEnvVar(env []interface{}, name, value string) []interface{} {
|
||||
// Check if variable already exists
|
||||
for i, envVar := range env {
|
||||
envVarMap, ok := envVar.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if envVarMap["name"] == name {
|
||||
// Update existing variable
|
||||
envVarMap["value"] = value
|
||||
env[i] = envVarMap
|
||||
return env
|
||||
}
|
||||
}
|
||||
|
||||
// Add new variable
|
||||
env = append(env, map[string]interface{}{
|
||||
"name": name,
|
||||
"value": value,
|
||||
})
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package fluxinstall
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
//go:embed manifests/*.yaml
|
||||
var embeddedFluxManifests embed.FS
|
||||
|
||||
// WriteEmbeddedManifests extracts embedded Flux manifests to a temporary directory.
|
||||
func WriteEmbeddedManifests(dir string) error {
|
||||
manifests, err := fs.ReadDir(embeddedFluxManifests, "manifests")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read embedded manifests: %w", err)
|
||||
}
|
||||
|
||||
for _, manifest := range manifests {
|
||||
data, err := fs.ReadFile(embeddedFluxManifests, path.Join("manifests", manifest.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file %s: %w", manifest.Name(), err)
|
||||
}
|
||||
|
||||
outputPath := path.Join(dir, manifest.Name())
|
||||
if err := os.WriteFile(outputPath, data, 0666); err != nil {
|
||||
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,963 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package operator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
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"
|
||||
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/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
const (
|
||||
// AnnotationSkipCozystackValues disables injection of cozystack-values secret into HelmRelease
|
||||
// This annotation should be placed on PackageSource
|
||||
AnnotationSkipCozystackValues = "operator.cozystack.io/skip-cozystack-values"
|
||||
// SecretCozystackValues is the name of the secret containing cluster and namespace configuration
|
||||
SecretCozystackValues = "cozystack-values"
|
||||
)
|
||||
|
||||
// PackageReconciler reconciles Package resources
|
||||
type PackageReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packages,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packages/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packagesources,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop
|
||||
func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
pkg := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, req.NamespacedName, pkg); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Resource not found, return (ownerReference will handle cleanup)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Get PackageSource with the same name
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "PackageSourceNotFound",
|
||||
Message: fmt.Sprintf("PackageSource %s not found", pkg.Name),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Determine variant (default to "default" if not specified)
|
||||
variantName := pkg.Spec.Variant
|
||||
if variantName == "" {
|
||||
variantName = "default"
|
||||
}
|
||||
|
||||
// Find the variant in PackageSource
|
||||
var variant *cozyv1alpha1.Variant
|
||||
for i := range packageSource.Spec.Variants {
|
||||
if packageSource.Spec.Variants[i].Name == variantName {
|
||||
variant = &packageSource.Spec.Variants[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if variant == nil {
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "VariantNotFound",
|
||||
Message: fmt.Sprintf("Variant %s not found in PackageSource %s", variantName, pkg.Name),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Reconcile namespaces from components
|
||||
if err := r.reconcileNamespaces(ctx, pkg, variant); err != nil {
|
||||
logger.Error(err, "failed to reconcile namespaces")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Update dependencies status
|
||||
if err := r.updateDependenciesStatus(ctx, pkg, variant); err != nil {
|
||||
logger.Error(err, "failed to update dependencies status")
|
||||
// Don't return error, continue with reconciliation
|
||||
}
|
||||
|
||||
// Validate variant dependencies before creating HelmReleases
|
||||
// Check if all dependencies are ready based on status
|
||||
if !r.areDependenciesReady(pkg, variant) {
|
||||
logger.Info("variant dependencies not ready, skipping HelmRelease creation", "package", pkg.Name)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "DependenciesNotReady",
|
||||
Message: "One or more dependencies are not ready",
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// Return success to avoid requeue, but don't create HelmReleases
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Create HelmReleases for components with Install section
|
||||
helmReleaseCount := 0
|
||||
for _, component := range variant.Components {
|
||||
// Skip components without Install section
|
||||
if component.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if component is disabled via Package spec
|
||||
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok {
|
||||
if pkgComponent.Enabled != nil && !*pkgComponent.Enabled {
|
||||
logger.V(1).Info("skipping disabled component", "package", pkg.Name, "component", component.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Build artifact name: <packagesource>-<variant>-<componentname> (with dots replaced by dashes)
|
||||
artifactName := fmt.Sprintf("%s-%s-%s",
|
||||
strings.ReplaceAll(packageSource.Name, ".", "-"),
|
||||
strings.ReplaceAll(variantName, ".", "-"),
|
||||
strings.ReplaceAll(component.Name, ".", "-"))
|
||||
|
||||
// Namespace must be set
|
||||
namespace := component.Install.Namespace
|
||||
if namespace == "" {
|
||||
logger.Error(fmt.Errorf("component %s has empty namespace in Install section", component.Name), "namespace validation failed")
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "InvalidConfiguration",
|
||||
Message: fmt.Sprintf("Component %s has empty namespace in Install section", component.Name),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("component %s has empty namespace in Install section", component.Name)
|
||||
}
|
||||
|
||||
// Determine release name (from Install or use component name)
|
||||
releaseName := component.Install.ReleaseName
|
||||
if releaseName == "" {
|
||||
releaseName = component.Name
|
||||
}
|
||||
|
||||
// Build labels
|
||||
labels := make(map[string]string)
|
||||
labels["cozystack.io/package"] = pkg.Name
|
||||
if component.Install.Privileged {
|
||||
labels["cozystack.io/privileged"] = "true"
|
||||
}
|
||||
|
||||
// Create HelmRelease
|
||||
hr := &helmv2.HelmRelease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: releaseName,
|
||||
Namespace: namespace,
|
||||
Labels: labels,
|
||||
},
|
||||
Spec: helmv2.HelmReleaseSpec{
|
||||
Interval: metav1.Duration{Duration: 5 * 60 * 1000000000}, // 5m
|
||||
ChartRef: &helmv2.CrossNamespaceSourceReference{
|
||||
Kind: "ExternalArtifact",
|
||||
Name: artifactName,
|
||||
Namespace: "cozy-system",
|
||||
},
|
||||
Install: &helmv2.Install{
|
||||
Remediation: &helmv2.InstallRemediation{
|
||||
Retries: -1,
|
||||
},
|
||||
},
|
||||
Upgrade: &helmv2.Upgrade{
|
||||
Remediation: &helmv2.UpgradeRemediation{
|
||||
Retries: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add valuesFrom for cozystack-values secret unless disabled by annotation on PackageSource
|
||||
if packageSource.GetAnnotations()[AnnotationSkipCozystackValues] != "true" {
|
||||
hr.Spec.ValuesFrom = []helmv2.ValuesReference{
|
||||
{
|
||||
Kind: "Secret",
|
||||
Name: SecretCozystackValues,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Set ownerReference
|
||||
gvk, err := apiutil.GVKForObject(pkg, r.Scheme)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to get GVK for Package")
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "InternalError",
|
||||
Message: fmt.Sprintf("Failed to get GVK for Package: %v", err),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("failed to get GVK for Package: %w", err)
|
||||
}
|
||||
hr.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
Kind: gvk.Kind,
|
||||
Name: pkg.Name,
|
||||
UID: pkg.UID,
|
||||
Controller: func() *bool { b := true; return &b }(),
|
||||
},
|
||||
}
|
||||
|
||||
// Merge values from Package spec if provided
|
||||
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok && pkgComponent.Values != nil {
|
||||
hr.Spec.Values = pkgComponent.Values
|
||||
}
|
||||
|
||||
// Build DependsOn from component Install and variant DependsOn
|
||||
dependsOn, err := r.buildDependsOn(ctx, pkg, packageSource, variant, &component)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to build DependsOn", "component", component.Name)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "DependsOnFailed",
|
||||
Message: fmt.Sprintf("Failed to build DependsOn for component %s: %v", component.Name, err),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// Return nil to stop reconciliation, error is recorded in status
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if len(dependsOn) > 0 {
|
||||
hr.Spec.DependsOn = dependsOn
|
||||
}
|
||||
|
||||
// Set valuesFiles annotation
|
||||
if len(component.ValuesFiles) > 0 {
|
||||
if hr.Annotations == nil {
|
||||
hr.Annotations = make(map[string]string)
|
||||
}
|
||||
hr.Annotations["cozyhr.cozystack.io/values-files"] = strings.Join(component.ValuesFiles, ",")
|
||||
}
|
||||
|
||||
if err := r.createOrUpdateHelmRelease(ctx, hr); err != nil {
|
||||
logger.Error(err, "failed to reconcile HelmRelease", "name", releaseName, "namespace", namespace)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "HelmReleaseFailed",
|
||||
Message: fmt.Sprintf("Failed to create HelmRelease %s: %v", releaseName, err),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
helmReleaseCount++
|
||||
logger.Info("reconciled HelmRelease", "package", pkg.Name, "component", component.Name, "releaseName", releaseName, "namespace", namespace)
|
||||
}
|
||||
|
||||
// Cleanup orphaned HelmReleases
|
||||
if err := r.cleanupOrphanedHelmReleases(ctx, pkg, variant); err != nil {
|
||||
logger.Error(err, "failed to cleanup orphaned HelmReleases")
|
||||
// Don't return error, continue with status update
|
||||
}
|
||||
|
||||
// Update status with success message
|
||||
message := fmt.Sprintf("reconciliation succeeded, generated %d helmrelease(s)", helmReleaseCount)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "ReconciliationSucceeded",
|
||||
Message: message,
|
||||
})
|
||||
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
logger.Info("reconciled Package", "name", pkg.Name, "helmReleaseCount", helmReleaseCount)
|
||||
|
||||
// Update dependencies status for Packages that depend on this Package
|
||||
// This ensures they get re-enqueued when their dependency becomes ready
|
||||
if err := r.updateDependentPackagesDependencies(ctx, pkg.Name); err != nil {
|
||||
logger.V(1).Error(err, "failed to update dependent packages dependencies", "package", pkg.Name)
|
||||
// Don't return error, this is best-effort
|
||||
}
|
||||
|
||||
// Dependent Packages will be automatically enqueued by the watch handler
|
||||
// when this Package's status is updated (see SetupWithManager watch handler)
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// createOrUpdateHelmRelease creates or updates a HelmRelease
|
||||
func (r *PackageReconciler) createOrUpdateHelmRelease(ctx context.Context, hr *helmv2.HelmRelease) error {
|
||||
existing := &helmv2.HelmRelease{}
|
||||
key := types.NamespacedName{
|
||||
Name: hr.Name,
|
||||
Namespace: hr.Namespace,
|
||||
}
|
||||
|
||||
err := r.Get(ctx, key, existing)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return r.Create(ctx, hr)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve resource version
|
||||
hr.SetResourceVersion(existing.GetResourceVersion())
|
||||
|
||||
// Merge labels
|
||||
labels := hr.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetLabels() {
|
||||
if _, ok := labels[k]; !ok {
|
||||
labels[k] = v
|
||||
}
|
||||
}
|
||||
hr.SetLabels(labels)
|
||||
|
||||
// Merge annotations
|
||||
annotations := hr.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetAnnotations() {
|
||||
if _, ok := annotations[k]; !ok {
|
||||
annotations[k] = v
|
||||
}
|
||||
}
|
||||
hr.SetAnnotations(annotations)
|
||||
|
||||
// Update Spec
|
||||
existing.Spec = hr.Spec
|
||||
existing.SetLabels(hr.GetLabels())
|
||||
existing.SetAnnotations(hr.GetAnnotations())
|
||||
existing.SetOwnerReferences(hr.GetOwnerReferences())
|
||||
|
||||
return r.Update(ctx, existing)
|
||||
}
|
||||
|
||||
// getVariantForPackage retrieves the Variant for a given Package
|
||||
// Returns the Variant and an error if not found
|
||||
// If c is nil, uses the reconciler's client
|
||||
func (r *PackageReconciler) getVariantForPackage(ctx context.Context, pkg *cozyv1alpha1.Package, c client.Client) (*cozyv1alpha1.Variant, error) {
|
||||
// Use provided client or fall back to reconciler's client
|
||||
cl := c
|
||||
if cl == nil {
|
||||
cl = r.Client
|
||||
}
|
||||
|
||||
// Determine variant name (default to "default" if not specified)
|
||||
variantName := pkg.Spec.Variant
|
||||
if variantName == "" {
|
||||
variantName = "default"
|
||||
}
|
||||
|
||||
// Get the PackageSource
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := cl.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("PackageSource %s not found", pkg.Name)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get PackageSource %s: %w", pkg.Name, err)
|
||||
}
|
||||
|
||||
// Find the variant in PackageSource
|
||||
var variant *cozyv1alpha1.Variant
|
||||
for i := range packageSource.Spec.Variants {
|
||||
if packageSource.Spec.Variants[i].Name == variantName {
|
||||
variant = &packageSource.Spec.Variants[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if variant == nil {
|
||||
return nil, fmt.Errorf("variant %s not found in PackageSource %s", variantName, pkg.Name)
|
||||
}
|
||||
|
||||
return variant, nil
|
||||
}
|
||||
|
||||
// buildDependsOn builds DependsOn list for a component
|
||||
// Includes:
|
||||
// 1. Dependencies from component.Install.DependsOn (with namespace from referenced component)
|
||||
// 2. Dependencies from variant.DependsOn (all components with Install from referenced Package)
|
||||
func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha1.Package, packageSource *cozyv1alpha1.PackageSource, variant *cozyv1alpha1.Variant, component *cozyv1alpha1.Component) ([]helmv2.DependencyReference, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
dependsOn := []helmv2.DependencyReference{}
|
||||
|
||||
// Build map of component names to their release names and namespaces in current variant
|
||||
componentMap := make(map[string]struct {
|
||||
releaseName string
|
||||
namespace string
|
||||
})
|
||||
for _, comp := range variant.Components {
|
||||
if comp.Install == nil {
|
||||
continue
|
||||
}
|
||||
compNamespace := comp.Install.Namespace
|
||||
if compNamespace == "" {
|
||||
return nil, fmt.Errorf("component %s has empty namespace in Install section", comp.Name)
|
||||
}
|
||||
compReleaseName := comp.Install.ReleaseName
|
||||
if compReleaseName == "" {
|
||||
compReleaseName = comp.Name
|
||||
}
|
||||
componentMap[comp.Name] = struct {
|
||||
releaseName string
|
||||
namespace string
|
||||
}{
|
||||
releaseName: compReleaseName,
|
||||
namespace: compNamespace,
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependencies from component.Install.DependsOn
|
||||
if len(component.Install.DependsOn) > 0 {
|
||||
for _, depName := range component.Install.DependsOn {
|
||||
depComp, ok := componentMap[depName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("component %s not found in variant for dependency %s", depName, component.Name)
|
||||
}
|
||||
dependsOn = append(dependsOn, helmv2.DependencyReference{
|
||||
Name: depComp.releaseName,
|
||||
Namespace: depComp.namespace,
|
||||
})
|
||||
logger.V(1).Info("added component dependency", "component", component.Name, "dependsOn", depName, "releaseName", depComp.releaseName, "namespace", depComp.namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependencies from variant.DependsOn
|
||||
if len(variant.DependsOn) > 0 {
|
||||
for _, depPackageName := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == depPackageName {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the Package
|
||||
depPackage := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("dependent Package %s not found", depPackageName)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get dependent Package %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Get the variant from dependent Package
|
||||
depVariant, err := r.getVariantForPackage(ctx, depPackage, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get variant for dependent Package %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Add all components with Install from dependent variant
|
||||
for _, depComp := range depVariant.Components {
|
||||
if depComp.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if component is disabled in dependent Package
|
||||
if depPkgComponent, ok := depPackage.Spec.Components[depComp.Name]; ok {
|
||||
if depPkgComponent.Enabled != nil && !*depPkgComponent.Enabled {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
depCompNamespace := depComp.Install.Namespace
|
||||
if depCompNamespace == "" {
|
||||
return nil, fmt.Errorf("component %s in dependent Package %s has empty namespace in Install section", depComp.Name, depPackageName)
|
||||
}
|
||||
depCompReleaseName := depComp.Install.ReleaseName
|
||||
if depCompReleaseName == "" {
|
||||
depCompReleaseName = depComp.Name
|
||||
}
|
||||
|
||||
dependsOn = append(dependsOn, helmv2.DependencyReference{
|
||||
Name: depCompReleaseName,
|
||||
Namespace: depCompNamespace,
|
||||
})
|
||||
logger.V(1).Info("added variant dependency", "package", pkg.Name, "dependency", depPackageName, "component", depComp.Name, "releaseName", depCompReleaseName, "namespace", depCompNamespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dependsOn, nil
|
||||
}
|
||||
|
||||
// updateDependenciesStatus updates the dependencies status in Package status
|
||||
// It checks the readiness of each dependency and updates pkg.Status.Dependencies
|
||||
// Old dependency keys that are no longer in the dependency list are removed
|
||||
func (r *PackageReconciler) updateDependenciesStatus(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Initialize dependencies map if nil
|
||||
if pkg.Status.Dependencies == nil {
|
||||
pkg.Status.Dependencies = make(map[string]cozyv1alpha1.DependencyStatus)
|
||||
}
|
||||
|
||||
// Build set of current dependencies (excluding ignored ones)
|
||||
currentDeps := make(map[string]bool)
|
||||
if len(variant.DependsOn) > 0 {
|
||||
for _, depPackageName := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == depPackageName {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
currentDeps[depPackageName] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old dependencies that are no longer in the list
|
||||
for depName := range pkg.Status.Dependencies {
|
||||
if !currentDeps[depName] {
|
||||
delete(pkg.Status.Dependencies, depName)
|
||||
logger.V(1).Info("removed old dependency from status", "package", pkg.Name, "dependency", depName)
|
||||
}
|
||||
}
|
||||
|
||||
// Update status for each current dependency
|
||||
for depPackageName := range currentDeps {
|
||||
// Get the Package
|
||||
depPackage := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Dependency not found, mark as not ready
|
||||
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
|
||||
Ready: false,
|
||||
}
|
||||
logger.V(1).Info("dependency not found, marking as not ready", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
// Error getting dependency, keep existing status or mark as not ready
|
||||
if _, exists := pkg.Status.Dependencies[depPackageName]; !exists {
|
||||
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
|
||||
Ready: false,
|
||||
}
|
||||
}
|
||||
logger.V(1).Error(err, "failed to get dependency, keeping existing status", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check Ready condition
|
||||
readyCondition := meta.FindStatusCondition(depPackage.Status.Conditions, "Ready")
|
||||
isReady := readyCondition != nil && readyCondition.Status == metav1.ConditionTrue
|
||||
|
||||
// Update dependency status
|
||||
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
|
||||
Ready: isReady,
|
||||
}
|
||||
logger.V(1).Info("updated dependency status", "package", pkg.Name, "dependency", depPackageName, "ready", isReady)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// areDependenciesReady checks if all dependencies are ready based on status
|
||||
func (r *PackageReconciler) areDependenciesReady(pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) bool {
|
||||
if len(variant.DependsOn) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, depPackageName := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == depPackageName {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check dependency status
|
||||
depStatus, exists := pkg.Status.Dependencies[depPackageName]
|
||||
if !exists || !depStatus.Ready {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// updateDependentPackagesDependencies updates dependencies status for all Packages that depend on the given Package
|
||||
// This ensures dependent packages get re-enqueued when their dependency status changes
|
||||
func (r *PackageReconciler) updateDependentPackagesDependencies(ctx context.Context, packageName string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Get all Packages
|
||||
packageList := &cozyv1alpha1.PackageList{}
|
||||
if err := r.List(ctx, packageList); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
// Get the updated Package to check its readiness
|
||||
updatedPkg := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: packageName}, updatedPkg); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil // Package not found, nothing to update
|
||||
}
|
||||
return fmt.Errorf("failed to get Package %s: %w", packageName, err)
|
||||
}
|
||||
|
||||
// Check Ready condition of the updated Package
|
||||
readyCondition := meta.FindStatusCondition(updatedPkg.Status.Conditions, "Ready")
|
||||
isReady := readyCondition != nil && readyCondition.Status == metav1.ConditionTrue
|
||||
|
||||
// For each Package, check if it depends on the given Package
|
||||
for _, pkg := range packageList.Items {
|
||||
// Skip the Package itself
|
||||
if pkg.Name == packageName {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get variant
|
||||
variant, err := r.getVariantForPackage(ctx, &pkg, nil)
|
||||
if err != nil {
|
||||
// Continue if PackageSource or variant not found (best-effort operation)
|
||||
logger.V(1).Info("skipping package, failed to get variant", "package", pkg.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this Package depends on the given Package
|
||||
dependsOn := false
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == dep {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
|
||||
if dep == packageName {
|
||||
dependsOn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if dependsOn {
|
||||
// Update the dependency status in this Package
|
||||
if pkg.Status.Dependencies == nil {
|
||||
pkg.Status.Dependencies = make(map[string]cozyv1alpha1.DependencyStatus)
|
||||
}
|
||||
pkg.Status.Dependencies[packageName] = cozyv1alpha1.DependencyStatus{
|
||||
Ready: isReady,
|
||||
}
|
||||
if err := r.Status().Update(ctx, &pkg); err != nil {
|
||||
logger.V(1).Error(err, "failed to update dependency status for dependent Package", "package", pkg.Name, "dependency", packageName)
|
||||
continue
|
||||
}
|
||||
logger.V(1).Info("updated dependency status for dependent Package", "package", pkg.Name, "dependency", packageName, "ready", isReady)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcileNamespaces creates or updates namespaces based on components in the variant
|
||||
func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Collect namespaces from components
|
||||
// Map: namespace -> {isPrivileged}
|
||||
type namespaceInfo struct {
|
||||
privileged bool
|
||||
}
|
||||
namespacesMap := make(map[string]namespaceInfo)
|
||||
|
||||
for _, component := range variant.Components {
|
||||
// Skip components without Install section
|
||||
if component.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if component is disabled via Package spec
|
||||
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok {
|
||||
if pkgComponent.Enabled != nil && !*pkgComponent.Enabled {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace must be set
|
||||
namespace := component.Install.Namespace
|
||||
if namespace == "" {
|
||||
return fmt.Errorf("component %s has empty namespace in Install section", component.Name)
|
||||
}
|
||||
|
||||
info, exists := namespacesMap[namespace]
|
||||
if !exists {
|
||||
info = namespaceInfo{
|
||||
privileged: false,
|
||||
}
|
||||
}
|
||||
|
||||
// If component is privileged, mark namespace as privileged
|
||||
if component.Install.Privileged {
|
||||
info.privileged = true
|
||||
}
|
||||
|
||||
namespacesMap[namespace] = info
|
||||
}
|
||||
|
||||
// Create or update all namespaces
|
||||
for nsName, info := range namespacesMap {
|
||||
namespace := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: nsName,
|
||||
Labels: make(map[string]string),
|
||||
Annotations: map[string]string{
|
||||
"helm.sh/resource-policy": "keep",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add system label only for non-tenant namespaces
|
||||
if !strings.HasPrefix(nsName, "tenant-") {
|
||||
namespace.Labels["cozystack.io/system"] = "true"
|
||||
}
|
||||
|
||||
// Add privileged label if needed
|
||||
if info.privileged {
|
||||
namespace.Labels["pod-security.kubernetes.io/enforce"] = "privileged"
|
||||
}
|
||||
|
||||
if err := r.createOrUpdateNamespace(ctx, namespace); err != nil {
|
||||
logger.Error(err, "failed to reconcile namespace", "name", nsName, "privileged", info.privileged)
|
||||
return fmt.Errorf("failed to reconcile namespace %s: %w", nsName, err)
|
||||
}
|
||||
logger.Info("reconciled namespace", "name", nsName, "privileged", info.privileged)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createOrUpdateNamespace creates or updates a namespace using server-side apply
|
||||
func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace) error {
|
||||
// Ensure TypeMeta is set for server-side apply
|
||||
namespace.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Namespace"))
|
||||
|
||||
// Use server-side apply with field manager
|
||||
// This is atomic and avoids race conditions from Get/Create/Update pattern
|
||||
// Labels and annotations will be merged automatically by the server
|
||||
// Each label/annotation key is treated as a separate field, so existing ones are preserved
|
||||
return r.Patch(ctx, namespace, client.Apply, client.FieldOwner("cozystack-package-controller"))
|
||||
}
|
||||
|
||||
// cleanupOrphanedHelmReleases removes HelmReleases that are no longer needed
|
||||
func (r *PackageReconciler) cleanupOrphanedHelmReleases(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Build map of desired HelmRelease names (from components with Install)
|
||||
desiredReleases := make(map[types.NamespacedName]bool)
|
||||
for _, component := range variant.Components {
|
||||
if component.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if component is disabled via Package spec
|
||||
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok {
|
||||
if pkgComponent.Enabled != nil && !*pkgComponent.Enabled {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
namespace := component.Install.Namespace
|
||||
if namespace == "" {
|
||||
// Skip components with empty namespace (they shouldn't exist anyway)
|
||||
continue
|
||||
}
|
||||
|
||||
releaseName := component.Install.ReleaseName
|
||||
if releaseName == "" {
|
||||
releaseName = component.Name
|
||||
}
|
||||
|
||||
desiredReleases[types.NamespacedName{
|
||||
Name: releaseName,
|
||||
Namespace: namespace,
|
||||
}] = true
|
||||
}
|
||||
|
||||
// Find all HelmReleases owned by this Package
|
||||
hrList := &helmv2.HelmReleaseList{}
|
||||
if err := r.List(ctx, hrList, client.MatchingLabels{
|
||||
"cozystack.io/package": pkg.Name,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete HelmReleases that are not in desired list
|
||||
for _, hr := range hrList.Items {
|
||||
key := types.NamespacedName{
|
||||
Name: hr.Name,
|
||||
Namespace: hr.Namespace,
|
||||
}
|
||||
if !desiredReleases[key] {
|
||||
logger.Info("deleting orphaned HelmRelease", "name", hr.Name, "namespace", hr.Namespace, "package", pkg.Name)
|
||||
if err := r.Delete(ctx, &hr); err != nil && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "failed to delete orphaned HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *PackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystack-package").
|
||||
For(&cozyv1alpha1.Package{}).
|
||||
Owns(&helmv2.HelmRelease{}).
|
||||
Watches(
|
||||
&cozyv1alpha1.PackageSource{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
ps, ok := obj.(*cozyv1alpha1.PackageSource)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Find Package with the same name as PackageSource
|
||||
// PackageSource and Package share the same name
|
||||
pkg := &cozyv1alpha1.Package{}
|
||||
if err := mgr.GetClient().Get(ctx, types.NamespacedName{Name: ps.Name}, pkg); err != nil {
|
||||
// Package not found, that's ok - it might not exist yet
|
||||
return nil
|
||||
}
|
||||
// Trigger reconcile for the corresponding Package
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pkg.Name,
|
||||
},
|
||||
}}
|
||||
}),
|
||||
).
|
||||
Watches(
|
||||
&cozyv1alpha1.Package{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
updatedPkg, ok := obj.(*cozyv1alpha1.Package)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Find all Packages that depend on this Package
|
||||
packageList := &cozyv1alpha1.PackageList{}
|
||||
if err := mgr.GetClient().List(ctx, packageList); err != nil {
|
||||
return nil
|
||||
}
|
||||
var requests []reconcile.Request
|
||||
for _, pkg := range packageList.Items {
|
||||
if pkg.Name == updatedPkg.Name {
|
||||
continue // Skip the Package itself
|
||||
}
|
||||
// Get variant to check dependencies
|
||||
variant, err := r.getVariantForPackage(ctx, &pkg, mgr.GetClient())
|
||||
if err != nil {
|
||||
// Continue if PackageSource or variant not found
|
||||
continue
|
||||
}
|
||||
// Check if this variant depends on updatedPkg
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == dep {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
if dep == updatedPkg.Name {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pkg.Name,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -1,413 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package operator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
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"
|
||||
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"
|
||||
)
|
||||
|
||||
// PackageSourceReconciler reconciles PackageSource resources
|
||||
type PackageSourceReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packagesources,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packagesources/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=source.extensions.fluxcd.io,resources=artifactgenerators,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop
|
||||
func (r *PackageSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, req.NamespacedName, packageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Resource not found, return (ownerReference will handle cleanup)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Generate ArtifactGenerator for package source
|
||||
if err := r.reconcileArtifactGenerators(ctx, packageSource); err != nil {
|
||||
logger.Error(err, "failed to reconcile ArtifactGenerator")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Update PackageSource status (variants and conditions from ArtifactGenerator)
|
||||
if err := r.updateStatus(ctx, packageSource); err != nil {
|
||||
logger.Error(err, "failed to update status")
|
||||
// Don't return error, status update is not critical
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// reconcileArtifactGenerators generates a single ArtifactGenerator for the package source
|
||||
// Creates one ArtifactGenerator per package source with all OutputArtifacts from components
|
||||
func (r *PackageSourceReconciler) reconcileArtifactGenerators(ctx context.Context, packageSource *cozyv1alpha1.PackageSource) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Check if SourceRef is set
|
||||
if packageSource.Spec.SourceRef == nil {
|
||||
logger.Info("skipping ArtifactGenerator creation, SourceRef not set", "packageSource", packageSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Namespace is always cozy-system
|
||||
namespace := "cozy-system"
|
||||
// ArtifactGenerator name is the package source name
|
||||
agName := packageSource.Name
|
||||
|
||||
// Collect all OutputArtifacts
|
||||
outputArtifacts := []sourcewatcherv1beta1.OutputArtifact{}
|
||||
|
||||
// Process all variants and their components
|
||||
for _, variant := range packageSource.Spec.Variants {
|
||||
// Build library map for this variant
|
||||
// Map key is the library name (from lib.Name or extracted from path)
|
||||
// This allows components in this variant to reference libraries by name
|
||||
// Libraries are scoped per variant to avoid conflicts between variants
|
||||
libraryMap := make(map[string]cozyv1alpha1.Library)
|
||||
for _, lib := range variant.Libraries {
|
||||
libName := lib.Name
|
||||
if libName == "" {
|
||||
// If library name is not set, extract from path
|
||||
libName = r.getPackageNameFromPath(lib.Path)
|
||||
}
|
||||
if libName != "" {
|
||||
// Store library with the resolved name
|
||||
libraryMap[libName] = lib
|
||||
}
|
||||
}
|
||||
|
||||
for _, component := range variant.Components {
|
||||
// Skip components without path
|
||||
if component.Path == "" {
|
||||
logger.V(1).Info("skipping component without path", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.V(1).Info("processing component", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name, "path", component.Path)
|
||||
|
||||
// Extract component name from path (last component)
|
||||
componentPathName := r.getPackageNameFromPath(component.Path)
|
||||
if componentPathName == "" {
|
||||
logger.Info("skipping component with invalid path", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name, "path", component.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get basePath with default values
|
||||
basePath := r.getBasePath(packageSource)
|
||||
|
||||
// Build copy operations
|
||||
copyOps := []sourcewatcherv1beta1.CopyOperation{
|
||||
{
|
||||
From: r.buildSourcePath(packageSource.Spec.SourceRef.Name, basePath, component.Path),
|
||||
To: fmt.Sprintf("@artifact/%s/", componentPathName),
|
||||
},
|
||||
}
|
||||
|
||||
// Add libraries if specified
|
||||
for _, libName := range component.Libraries {
|
||||
if lib, ok := libraryMap[libName]; ok {
|
||||
copyOps = append(copyOps, sourcewatcherv1beta1.CopyOperation{
|
||||
From: r.buildSourcePath(packageSource.Spec.SourceRef.Name, basePath, lib.Path),
|
||||
To: fmt.Sprintf("@artifact/%s/charts/%s/", componentPathName, libName),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add valuesFiles if specified
|
||||
for i, valuesFile := range component.ValuesFiles {
|
||||
strategy := "Merge"
|
||||
if i == 0 {
|
||||
strategy = "Overwrite"
|
||||
}
|
||||
copyOps = append(copyOps, sourcewatcherv1beta1.CopyOperation{
|
||||
From: r.buildSourceFilePath(packageSource.Spec.SourceRef.Name, basePath, fmt.Sprintf("%s/%s", component.Path, valuesFile)),
|
||||
To: fmt.Sprintf("@artifact/%s/values.yaml", componentPathName),
|
||||
Strategy: strategy,
|
||||
})
|
||||
}
|
||||
|
||||
// Artifact name: <packagesource>-<variant>-<componentname>
|
||||
// Replace dots with dashes to comply with Kubernetes naming requirements
|
||||
artifactName := fmt.Sprintf("%s-%s-%s",
|
||||
strings.ReplaceAll(packageSource.Name, ".", "-"),
|
||||
strings.ReplaceAll(variant.Name, ".", "-"),
|
||||
strings.ReplaceAll(component.Name, ".", "-"))
|
||||
|
||||
outputArtifacts = append(outputArtifacts, sourcewatcherv1beta1.OutputArtifact{
|
||||
Name: artifactName,
|
||||
Copy: copyOps,
|
||||
})
|
||||
|
||||
logger.Info("added OutputArtifact for component", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name, "artifactName", artifactName)
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no OutputArtifacts, return (ownerReference will handle cleanup if needed)
|
||||
if len(outputArtifacts) == 0 {
|
||||
logger.Info("no OutputArtifacts to generate, skipping ArtifactGenerator creation", "packageSource", packageSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build labels
|
||||
labels := make(map[string]string)
|
||||
labels["cozystack.io/packagesource"] = packageSource.Name
|
||||
|
||||
// Create single ArtifactGenerator for the package source
|
||||
ag := &sourcewatcherv1beta1.ArtifactGenerator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: agName,
|
||||
Namespace: namespace,
|
||||
Labels: labels,
|
||||
},
|
||||
Spec: sourcewatcherv1beta1.ArtifactGeneratorSpec{
|
||||
Sources: []sourcewatcherv1beta1.SourceReference{
|
||||
{
|
||||
Alias: packageSource.Spec.SourceRef.Name,
|
||||
Kind: packageSource.Spec.SourceRef.Kind,
|
||||
Name: packageSource.Spec.SourceRef.Name,
|
||||
Namespace: packageSource.Spec.SourceRef.Namespace,
|
||||
},
|
||||
},
|
||||
OutputArtifacts: outputArtifacts,
|
||||
},
|
||||
}
|
||||
|
||||
// Set ownerReference
|
||||
gvk, err := apiutil.GVKForObject(packageSource, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get GVK for PackageSource: %w", err)
|
||||
}
|
||||
ag.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
Kind: gvk.Kind,
|
||||
Name: packageSource.Name,
|
||||
UID: packageSource.UID,
|
||||
Controller: func() *bool { b := true; return &b }(),
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info("creating ArtifactGenerator for package source", "packageSource", packageSource.Name, "agName", agName, "namespace", namespace, "outputArtifactCount", len(outputArtifacts))
|
||||
|
||||
if err := r.createOrUpdate(ctx, ag); err != nil {
|
||||
return fmt.Errorf("failed to reconcile ArtifactGenerator %s: %w", agName, err)
|
||||
}
|
||||
|
||||
logger.Info("reconciled ArtifactGenerator for package source", "name", agName, "namespace", namespace, "outputArtifactCount", len(outputArtifacts))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func (r *PackageSourceReconciler) getPackageNameFromPath(path string) string {
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getBasePath returns the basePath with default values based on source kind
|
||||
func (r *PackageSourceReconciler) getBasePath(packageSource *cozyv1alpha1.PackageSource) string {
|
||||
// If path is explicitly set in SourceRef, use it (but normalize "/" to empty)
|
||||
if packageSource.Spec.SourceRef.Path != "" {
|
||||
path := strings.Trim(packageSource.Spec.SourceRef.Path, "/")
|
||||
// If path is "/" or empty after trim, return empty string
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
return path
|
||||
}
|
||||
// Default values based on kind
|
||||
if packageSource.Spec.SourceRef.Kind == "OCIRepository" {
|
||||
return "" // Root for OCI
|
||||
}
|
||||
// Default for GitRepository
|
||||
return "packages"
|
||||
}
|
||||
|
||||
// buildSourcePath builds the full source path using basePath with glob pattern
|
||||
func (r *PackageSourceReconciler) buildSourcePath(sourceName, basePath, path string) string {
|
||||
// Remove leading/trailing slashes and combine
|
||||
parts := []string{}
|
||||
if basePath != "" {
|
||||
trimmed := strings.Trim(basePath, "/")
|
||||
if trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
}
|
||||
if path != "" {
|
||||
trimmed := strings.Trim(path, "/")
|
||||
if trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
fullPath := strings.Join(parts, "/")
|
||||
if fullPath == "" {
|
||||
return fmt.Sprintf("@%s/**", sourceName)
|
||||
}
|
||||
return fmt.Sprintf("@%s/%s/**", sourceName, fullPath)
|
||||
}
|
||||
|
||||
// buildSourceFilePath builds the full source path for a specific file (without glob pattern)
|
||||
func (r *PackageSourceReconciler) buildSourceFilePath(sourceName, basePath, path string) string {
|
||||
// Remove leading/trailing slashes and combine
|
||||
parts := []string{}
|
||||
if basePath != "" {
|
||||
trimmed := strings.Trim(basePath, "/")
|
||||
if trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
}
|
||||
if path != "" {
|
||||
trimmed := strings.Trim(path, "/")
|
||||
if trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
fullPath := strings.Join(parts, "/")
|
||||
if fullPath == "" {
|
||||
return fmt.Sprintf("@%s", sourceName)
|
||||
}
|
||||
return fmt.Sprintf("@%s/%s", sourceName, fullPath)
|
||||
}
|
||||
|
||||
// createOrUpdate creates or updates a resource using server-side apply
|
||||
func (r *PackageSourceReconciler) createOrUpdate(ctx context.Context, obj client.Object) error {
|
||||
// Ensure TypeMeta is set for server-side apply
|
||||
// Use type assertion to set GVK if the object supports it
|
||||
if runtimeObj, ok := obj.(runtime.Object); ok {
|
||||
gvk, err := apiutil.GVKForObject(obj, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get GVK for object: %w", err)
|
||||
}
|
||||
runtimeObj.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
|
||||
// Use server-side apply with field manager
|
||||
// This is atomic and avoids race conditions from Get/Create/Update pattern
|
||||
// Labels, annotations, and spec will be merged automatically by the server
|
||||
// Each field is treated separately, so existing ones are preserved
|
||||
return r.Patch(ctx, obj, client.Apply, client.FieldOwner("cozystack-packagesource-controller"))
|
||||
}
|
||||
|
||||
// updateStatus updates PackageSource status (variants and conditions from ArtifactGenerator)
|
||||
func (r *PackageSourceReconciler) updateStatus(ctx context.Context, packageSource *cozyv1alpha1.PackageSource) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Update variants in status from spec
|
||||
variantNames := make([]string, 0, len(packageSource.Spec.Variants))
|
||||
for _, variant := range packageSource.Spec.Variants {
|
||||
variantNames = append(variantNames, variant.Name)
|
||||
}
|
||||
packageSource.Status.Variants = strings.Join(variantNames, ",")
|
||||
|
||||
// Check if SourceRef is set
|
||||
if packageSource.Spec.SourceRef == nil {
|
||||
// Set status to unknown if SourceRef is not set
|
||||
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: "SourceRefNotSet",
|
||||
Message: "SourceRef is not configured",
|
||||
})
|
||||
return r.Status().Update(ctx, packageSource)
|
||||
}
|
||||
|
||||
// Get ArtifactGenerator
|
||||
ag := &sourcewatcherv1beta1.ArtifactGenerator{}
|
||||
agKey := types.NamespacedName{
|
||||
Name: packageSource.Name,
|
||||
Namespace: "cozy-system",
|
||||
}
|
||||
|
||||
if err := r.Get(ctx, agKey, ag); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// ArtifactGenerator not found, set status to unknown
|
||||
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: "ArtifactGeneratorNotFound",
|
||||
Message: "ArtifactGenerator not found",
|
||||
})
|
||||
return r.Status().Update(ctx, packageSource)
|
||||
}
|
||||
return fmt.Errorf("failed to get ArtifactGenerator: %w", err)
|
||||
}
|
||||
|
||||
// Find Ready condition in ArtifactGenerator
|
||||
readyCondition := meta.FindStatusCondition(ag.Status.Conditions, "Ready")
|
||||
if readyCondition == nil {
|
||||
// No Ready condition in ArtifactGenerator, set status to unknown
|
||||
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: "ArtifactGeneratorNotReady",
|
||||
Message: "ArtifactGenerator Ready condition not found",
|
||||
})
|
||||
return r.Status().Update(ctx, packageSource)
|
||||
}
|
||||
|
||||
// Copy Ready condition from ArtifactGenerator to PackageSource
|
||||
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: readyCondition.Status,
|
||||
Reason: readyCondition.Reason,
|
||||
Message: readyCondition.Message,
|
||||
ObservedGeneration: packageSource.Generation,
|
||||
LastTransitionTime: readyCondition.LastTransitionTime,
|
||||
})
|
||||
|
||||
logger.V(1).Info("updated PackageSource status from ArtifactGenerator",
|
||||
"packageSource", packageSource.Name,
|
||||
"status", readyCondition.Status,
|
||||
"reason", readyCondition.Reason)
|
||||
|
||||
return r.Status().Update(ctx, packageSource)
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *PackageSourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystack-packagesource").
|
||||
For(&cozyv1alpha1.PackageSource{}).
|
||||
Owns(&sourcewatcherv1beta1.ArtifactGenerator{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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:9e34fd50393b418d9516aadb488067a3a63675b045811beb1c0afc9c61e149e8
|
||||
|
||||
@@ -145,31 +145,31 @@ See the reference for components utilized in this service:
|
||||
|
||||
### Kubernetes Control Plane Configuration
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| --------------------------------------------------- | ------------------------------------------------ | ---------- | -------- |
|
||||
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
|
||||
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
|
||||
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `medium` |
|
||||
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| Name | Description | Type | Value |
|
||||
| --------------------------------------------------- | ------------------------------------------------ | ---------- | ------- |
|
||||
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
|
||||
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
|
||||
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `large` |
|
||||
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
|
||||
|
||||
## Parameter examples and reference
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:2d39989846c3579dd020b9f6c77e6e314cc81aa344eaac0f6d633e723c17196d
|
||||
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:6f2b1d6b0b2bdc66f1cbb30c59393369cbf070cb8f5fec748f176952273483cc
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.0.0@sha256:5335c044313b69ee13b30ca4941687e509005e55f4ae25723861edbf2fbd6dd2
|
||||
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.0.0@sha256:dee69d15fa8616aa6a1e5a67fc76370e7698a7f58b25e30650eb39c9fb826de8
|
||||
|
||||
@@ -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:15b94dca216b73336e7f39f4ea1b76b7656890d6be8a8cf0d9a786b4006781f9
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:a09724a7f95283f9130b3da2a89d81c4c6051c6edf0392a81b6fc90f404b76b6
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:71a74ca30f75967bae309be2758f19aa3d37c60b19426b9b622ff1c33a80362f
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
"resourcesPreset": {
|
||||
"description": "Preset if `resources` omitted.",
|
||||
"type": "string",
|
||||
"default": "medium",
|
||||
"default": "large",
|
||||
"enum": [
|
||||
"nano",
|
||||
"micro",
|
||||
|
||||
@@ -153,7 +153,7 @@ addons:
|
||||
|
||||
## @typedef {struct} APIServer - API Server configuration.
|
||||
## @field {Resources} resources - CPU and memory resources for API Server.
|
||||
## @field {ResourcesPreset} resourcesPreset="medium" - Preset if `resources` omitted.
|
||||
## @field {ResourcesPreset} resourcesPreset="large" - Preset if `resources` omitted.
|
||||
|
||||
## @typedef {struct} ControllerManager - Controller Manager configuration.
|
||||
## @field {Resources} resources - CPU and memory resources for Controller Manager.
|
||||
@@ -182,7 +182,7 @@ controlPlane:
|
||||
replicas: 2
|
||||
apiServer:
|
||||
resources: {}
|
||||
resourcesPreset: "medium"
|
||||
resourcesPreset: "large"
|
||||
controllerManager:
|
||||
resources: {}
|
||||
resourcesPreset: "micro"
|
||||
|
||||
23
packages/apps/mongodb/.helmignore
Normal file
23
packages/apps/mongodb/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
7
packages/apps/mongodb/Chart.yaml
Normal file
7
packages/apps/mongodb/Chart.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v2
|
||||
name: mongodb
|
||||
description: Managed MongoDB service
|
||||
icon: /logos/mongodb.svg
|
||||
type: application
|
||||
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
|
||||
appVersion: "8.0"
|
||||
11
packages/apps/mongodb/Makefile
Normal file
11
packages/apps/mongodb/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
include ../../../scripts/package.mk
|
||||
|
||||
.PHONY: generate update
|
||||
|
||||
generate:
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
../../../hack/update-crd.sh
|
||||
|
||||
update:
|
||||
hack/update-versions.sh
|
||||
make generate
|
||||
113
packages/apps/mongodb/README.md
Normal file
113
packages/apps/mongodb/README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Managed MongoDB Service
|
||||
|
||||
MongoDB is a popular document-oriented NoSQL database known for its flexibility and scalability.
|
||||
The Managed MongoDB Service provides a self-healing replicated cluster managed by the Percona Operator for MongoDB.
|
||||
|
||||
## Deployment Details
|
||||
|
||||
This managed service is controlled by the Percona Operator for MongoDB, ensuring efficient management and seamless operation.
|
||||
|
||||
- Docs: <https://docs.percona.com/percona-operator-for-mongodb/>
|
||||
- Github: <https://github.com/percona/percona-server-mongodb-operator>
|
||||
|
||||
## Deployment Modes
|
||||
|
||||
### Replica Set Mode (default)
|
||||
|
||||
By default, MongoDB deploys as a replica set with the specified number of replicas.
|
||||
This mode is suitable for most use cases requiring high availability.
|
||||
|
||||
### Sharded Cluster Mode
|
||||
|
||||
Enable `sharding: true` for horizontal scaling across multiple shards.
|
||||
Each shard is a replica set, and mongos routers handle query routing.
|
||||
|
||||
## Notes
|
||||
|
||||
### External Access
|
||||
|
||||
When `external: true` is enabled:
|
||||
- **Replica Set mode**: Traffic is load-balanced across all replica set members. This works well for read operations, but write operations require connecting to the primary. MongoDB drivers handle primary discovery automatically using the replica set connection string.
|
||||
- **Sharded mode**: Traffic is routed through mongos routers, which handle both reads and writes correctly.
|
||||
|
||||
### Credentials
|
||||
|
||||
On first install, the credentials secret will be empty until the Percona operator initializes the cluster.
|
||||
Run `helm upgrade` after MongoDB is ready to populate the credentials secret with the actual password.
|
||||
|
||||
## Parameters
|
||||
|
||||
### Common parameters
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | ------- |
|
||||
| `replicas` | Number of MongoDB replicas in replica set. | `int` | `3` |
|
||||
| `resources` | Explicit CPU and memory configuration for each MongoDB replica. When omitted, the preset defined in `resourcesPreset` is applied. | `object` | `{}` |
|
||||
| `resources.cpu` | CPU available to each replica. | `quantity` | `""` |
|
||||
| `resources.memory` | Memory (RAM) available to each replica. | `quantity` | `""` |
|
||||
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `small` |
|
||||
| `size` | Persistent Volume Claim size available for application data. | `quantity` | `10Gi` |
|
||||
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
|
||||
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
|
||||
| `version` | MongoDB major version to deploy. | `string` | `v8` |
|
||||
|
||||
|
||||
### Image configuration
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| --------------- | -------------------------------------- | -------- | --------------------------------------- |
|
||||
| `images` | Container images used by the operator. | `object` | `{}` |
|
||||
| `images.pmm` | PMM client image for monitoring. | `string` | `percona/pmm-client:2.44.1` |
|
||||
| `images.backup` | Percona Backup for MongoDB image. | `string` | `percona/percona-backup-mongodb:2.11.0` |
|
||||
|
||||
|
||||
### Sharding configuration
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| ----------------------------------- | ------------------------------------------------------------------ | ---------- | ------- |
|
||||
| `sharding` | Enable sharded cluster mode. When disabled, deploys a replica set. | `bool` | `false` |
|
||||
| `shardingConfig` | Configuration for sharded cluster mode. | `object` | `{}` |
|
||||
| `shardingConfig.configServers` | Number of config server replicas. | `int` | `3` |
|
||||
| `shardingConfig.configServerSize` | PVC size for config servers. | `quantity` | `3Gi` |
|
||||
| `shardingConfig.mongos` | Number of mongos router replicas. | `int` | `2` |
|
||||
| `shardingConfig.shards` | List of shard configurations. | `[]object` | `[...]` |
|
||||
| `shardingConfig.shards[i].name` | Shard name. | `string` | `""` |
|
||||
| `shardingConfig.shards[i].replicas` | Number of replicas in this shard. | `int` | `0` |
|
||||
| `shardingConfig.shards[i].size` | PVC size for this shard. | `quantity` | `""` |
|
||||
|
||||
|
||||
### Users configuration
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| --------------------------- | --------------------------------------------------- | ------------------- | ----- |
|
||||
| `users` | Custom MongoDB users configuration map. | `map[string]object` | `{}` |
|
||||
| `users[name].password` | Password for the user (auto-generated if omitted). | `string` | `""` |
|
||||
| `users[name].db` | Database to authenticate against. | `string` | `""` |
|
||||
| `users[name].roles` | List of MongoDB roles with database scope. | `[]object` | `[]` |
|
||||
| `users[name].roles[i].name` | Role name (e.g., readWrite, dbAdmin, clusterAdmin). | `string` | `""` |
|
||||
| `users[name].roles[i].db` | Database the role applies to. | `string` | `""` |
|
||||
|
||||
|
||||
### Backup parameters
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| ------------------------ | ------------------------------------------------------ | -------- | ----------------------------------- |
|
||||
| `backup` | Backup configuration. | `object` | `{}` |
|
||||
| `backup.enabled` | Enable regular backups. | `bool` | `false` |
|
||||
| `backup.schedule` | Cron schedule for automated backups. | `string` | `0 2 * * *` |
|
||||
| `backup.retentionPolicy` | Retention policy (e.g. "30d"). | `string` | `30d` |
|
||||
| `backup.destinationPath` | Destination path for backups (e.g. s3://bucket/path/). | `string` | `s3://bucket/path/to/folder/` |
|
||||
| `backup.endpointURL` | S3 endpoint URL for uploads. | `string` | `http://minio-gateway-service:9000` |
|
||||
| `backup.s3AccessKey` | Access key for S3 authentication. | `string` | `""` |
|
||||
| `backup.s3SecretKey` | Secret key for S3 authentication. | `string` | `""` |
|
||||
|
||||
|
||||
### Bootstrap (recovery) parameters
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| ------------------------ | --------------------------------------------------------- | -------- | ------- |
|
||||
| `bootstrap` | Bootstrap configuration. | `object` | `{}` |
|
||||
| `bootstrap.enabled` | Whether to restore from a backup. | `bool` | `false` |
|
||||
| `bootstrap.recoveryTime` | Timestamp for point-in-time recovery; empty means latest. | `string` | `""` |
|
||||
| `bootstrap.backupName` | Name of backup to restore from. | `string` | `""` |
|
||||
|
||||
1
packages/apps/mongodb/charts/cozy-lib
Symbolic link
1
packages/apps/mongodb/charts/cozy-lib
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../library/cozy-lib
|
||||
5
packages/apps/mongodb/files/versions.yaml
Normal file
5
packages/apps/mongodb/files/versions.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
# MongoDB version mapping (major version -> Percona image tag)
|
||||
# Auto-generated by hack/update-versions.sh - do not edit manually
|
||||
"v8": "8.0.17-6"
|
||||
"v7": "7.0.28-15"
|
||||
"v6": "6.0.25-20"
|
||||
125
packages/apps/mongodb/hack/update-versions.sh
Executable file
125
packages/apps/mongodb/hack/update-versions.sh
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MONGODB_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
VALUES_FILE="${MONGODB_DIR}/values.yaml"
|
||||
VERSIONS_FILE="${MONGODB_DIR}/files/versions.yaml"
|
||||
|
||||
# Supported major versions (newest first)
|
||||
SUPPORTED_MAJOR_VERSIONS="8 7 6"
|
||||
|
||||
echo "Supported major versions: $SUPPORTED_MAJOR_VERSIONS"
|
||||
|
||||
# Check if skopeo is installed
|
||||
if ! command -v skopeo &> /dev/null; then
|
||||
echo "Error: skopeo is not installed. Please install skopeo and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if jq is installed
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is not installed. Please install jq and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get available image tags from Percona registry
|
||||
echo "Fetching available image tags from registry..."
|
||||
AVAILABLE_TAGS=$(skopeo list-tags docker://percona/percona-server-mongodb | jq -r '.Tags[]' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+$' | sort -V)
|
||||
|
||||
if [ -z "$AVAILABLE_TAGS" ]; then
|
||||
echo "Error: Could not fetch available image tags" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build versions map: major version -> latest tag
|
||||
declare -A VERSION_MAP
|
||||
MAJOR_VERSIONS=()
|
||||
|
||||
for major_version in $SUPPORTED_MAJOR_VERSIONS; do
|
||||
# Find all tags that match this major version
|
||||
matching_tags=$(echo "$AVAILABLE_TAGS" | grep "^${major_version}\\.")
|
||||
|
||||
if [ -n "$matching_tags" ]; then
|
||||
# Get the latest tag for this major version
|
||||
latest_tag=$(echo "$matching_tags" | tail -n1)
|
||||
VERSION_MAP["v${major_version}"]="${latest_tag}"
|
||||
MAJOR_VERSIONS+=("v${major_version}")
|
||||
echo "Found version: v${major_version} -> ${latest_tag}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MAJOR_VERSIONS[@]} -eq 0 ]; then
|
||||
echo "Error: No matching versions found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Major versions to add: ${MAJOR_VERSIONS[*]}"
|
||||
|
||||
# Create/update versions.yaml file
|
||||
echo "Updating $VERSIONS_FILE..."
|
||||
{
|
||||
echo "# MongoDB version mapping (major version -> Percona image tag)"
|
||||
echo "# Auto-generated by hack/update-versions.sh - do not edit manually"
|
||||
for major_ver in "${MAJOR_VERSIONS[@]}"; do
|
||||
echo "\"${major_ver}\": \"${VERSION_MAP[$major_ver]}\""
|
||||
done
|
||||
} > "$VERSIONS_FILE"
|
||||
|
||||
echo "Successfully updated $VERSIONS_FILE"
|
||||
|
||||
# Update values.yaml - enum with major versions only
|
||||
TEMP_FILE=$(mktemp)
|
||||
trap 'rm -f "$TEMP_FILE" "${TEMP_FILE}.tmp"' EXIT
|
||||
|
||||
# Build new version section
|
||||
NEW_VERSION_SECTION="## @enum {string} Version"
|
||||
for major_ver in "${MAJOR_VERSIONS[@]}"; do
|
||||
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
|
||||
## @value $major_ver"
|
||||
done
|
||||
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
|
||||
|
||||
## @param {Version} version - MongoDB major version to deploy.
|
||||
version: ${MAJOR_VERSIONS[0]}"
|
||||
|
||||
# Check if version section already exists
|
||||
if grep -q "^## @enum {string} Version" "$VALUES_FILE"; then
|
||||
# Version section exists, update it using awk
|
||||
echo "Updating existing version section in $VALUES_FILE..."
|
||||
|
||||
# Use awk to replace the section from "## @enum {string} Version" to "version: " (inclusive)
|
||||
awk -v new_section="$NEW_VERSION_SECTION" '
|
||||
/^## @enum {string} Version/ {
|
||||
in_section = 1
|
||||
print new_section
|
||||
next
|
||||
}
|
||||
in_section && /^version: / {
|
||||
in_section = 0
|
||||
next
|
||||
}
|
||||
in_section {
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
|
||||
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
|
||||
else
|
||||
# Version section doesn't exist, insert it before Sharding section
|
||||
echo "Inserting new version section in $VALUES_FILE..."
|
||||
|
||||
awk -v new_section="$NEW_VERSION_SECTION" '
|
||||
/^## @section Sharding configuration/ {
|
||||
print new_section
|
||||
print ""
|
||||
}
|
||||
{ print }
|
||||
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
|
||||
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
|
||||
fi
|
||||
|
||||
echo "Successfully updated $VALUES_FILE with major versions: ${MAJOR_VERSIONS[*]}"
|
||||
13
packages/apps/mongodb/logos/mongodb.svg
Normal file
13
packages/apps/mongodb/logos/mongodb.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="144" height="144" viewBox="0 0 144 144" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="144" height="144" rx="24" fill="url(#paint0_linear_mongodb)"/>
|
||||
<path d="M72 24C72 24 72 24 72 24C72 24 58 40 58 62C58 84 72 120 72 120C72 120 86 84 86 62C86 40 72 24 72 24Z" fill="#00ED64"/>
|
||||
<path d="M72 120C72 120 86 84 86 62C86 40 72 24 72 24" stroke="#00684A" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M72 24C72 24 58 40 58 62C58 84 72 120 72 120" stroke="#001E2B" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="69" y="108" width="6" height="16" rx="2" fill="#00684A"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_mongodb" x1="140" y1="130.5" x2="4" y2="9.49999" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#001E2B"/>
|
||||
<stop offset="1" stop-color="#023430"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 871 B |
0
packages/apps/mongodb/templates/.gitkeep
Normal file
0
packages/apps/mongodb/templates/.gitkeep
Normal file
12
packages/apps/mongodb/templates/_versions.tpl
Normal file
12
packages/apps/mongodb/templates/_versions.tpl
Normal file
@@ -0,0 +1,12 @@
|
||||
{{/*
|
||||
MongoDB version mapping
|
||||
*/}}
|
||||
{{- define "mongodb.versionMap" -}}
|
||||
{{- $versions := .Files.Get "files/versions.yaml" | fromYaml -}}
|
||||
{{- $version := .Values.version -}}
|
||||
{{- if hasKey $versions $version -}}
|
||||
{{- index $versions $version -}}
|
||||
{{- else -}}
|
||||
{{- fail (printf "Unsupported MongoDB version: %s. Supported versions: %s" $version (keys $versions | sortAlpha | join ", ")) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
11
packages/apps/mongodb/templates/backup-secret.yaml
Normal file
11
packages/apps/mongodb/templates/backup-secret.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
{{- if or .Values.backup.enabled .Values.bootstrap.enabled }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-s3-creds
|
||||
type: Opaque
|
||||
stringData:
|
||||
AWS_ACCESS_KEY_ID: {{ required "backup.s3AccessKey is required when backup or bootstrap is enabled" .Values.backup.s3AccessKey | quote }}
|
||||
AWS_SECRET_ACCESS_KEY: {{ required "backup.s3SecretKey is required when backup or bootstrap is enabled" .Values.backup.s3SecretKey | quote }}
|
||||
{{- end }}
|
||||
34
packages/apps/mongodb/templates/credentials.yaml
Normal file
34
packages/apps/mongodb/templates/credentials.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
|
||||
{{- $operatorSecret := lookup "v1" "Secret" .Release.Namespace (printf "internal-%s-users" .Release.Name) }}
|
||||
{{- $password := "" }}
|
||||
{{- if and $operatorSecret (hasKey $operatorSecret.data "MONGODB_DATABASE_ADMIN_PASSWORD") }}
|
||||
{{- $password = index $operatorSecret.data "MONGODB_DATABASE_ADMIN_PASSWORD" | b64dec }}
|
||||
{{- end }}
|
||||
---
|
||||
# Dashboard credentials - lookup from operator-created secret
|
||||
# Operator creates secret named "internal-<release>-users" with system user passwords
|
||||
# Note: On first install, password/uri will be empty until operator creates the secret.
|
||||
# Run 'helm upgrade' after MongoDB is ready to populate credentials.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-credentials
|
||||
type: Opaque
|
||||
stringData:
|
||||
username: databaseAdmin
|
||||
password: {{ $password | quote }}
|
||||
{{- if .Values.sharding }}
|
||||
host: {{ .Release.Name }}-mongos.{{ .Release.Namespace }}.svc.{{ $clusterDomain }}
|
||||
{{- else }}
|
||||
host: {{ .Release.Name }}-rs0.{{ .Release.Namespace }}.svc.{{ $clusterDomain }}
|
||||
{{- end }}
|
||||
port: "27017"
|
||||
{{- if $password }}
|
||||
{{- if .Values.sharding }}
|
||||
uri: mongodb://databaseAdmin:{{ $password | urlquery }}@{{ .Release.Name }}-mongos.{{ .Release.Namespace }}.svc.{{ $clusterDomain }}:27017/admin
|
||||
{{- else }}
|
||||
uri: mongodb://databaseAdmin:{{ $password | urlquery }}@{{ .Release.Name }}-rs0.{{ .Release.Namespace }}.svc.{{ $clusterDomain }}:27017/admin?replicaSet=rs0
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
uri: ""
|
||||
{{- end }}
|
||||
39
packages/apps/mongodb/templates/dashboard-resourcemap.yaml
Normal file
39
packages/apps/mongodb/templates/dashboard-resourcemap.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-dashboard-resources
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services
|
||||
resourceNames:
|
||||
- {{ .Release.Name }}-rs0
|
||||
- {{ .Release.Name }}-mongos
|
||||
- {{ .Release.Name }}-external
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
resourceNames:
|
||||
- {{ .Release.Name }}-credentials
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups:
|
||||
- cozystack.io
|
||||
resources:
|
||||
- workloadmonitors
|
||||
resourceNames:
|
||||
- {{ .Release.Name }}
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-dashboard-resources
|
||||
subjects:
|
||||
{{ include "cozy-lib.rbac.subjectsForTenantAndAccessLevel" (list "use" .Release.Namespace) }}
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: {{ .Release.Name }}-dashboard-resources
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
24
packages/apps/mongodb/templates/external-svc.yaml
Normal file
24
packages/apps/mongodb/templates/external-svc.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
{{- if .Values.external }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-external
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
externalTrafficPolicy: Local
|
||||
{{- if (include "cozy-lib.network.disableLoadBalancerNodePorts" $ | fromYaml) }}
|
||||
allocateLoadBalancerNodePorts: false
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: mongodb
|
||||
port: 27017
|
||||
selector:
|
||||
app.kubernetes.io/name: percona-server-mongodb
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- if .Values.sharding }}
|
||||
app.kubernetes.io/component: mongos
|
||||
{{- else }}
|
||||
app.kubernetes.io/component: mongod
|
||||
app.kubernetes.io/replset: rs0
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
173
packages/apps/mongodb/templates/mongodb.yaml
Normal file
173
packages/apps/mongodb/templates/mongodb.yaml
Normal file
@@ -0,0 +1,173 @@
|
||||
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
|
||||
---
|
||||
apiVersion: psmdb.percona.com/v1
|
||||
kind: PerconaServerMongoDB
|
||||
metadata:
|
||||
name: {{ .Release.Name }}
|
||||
spec:
|
||||
crVersion: 1.21.1
|
||||
clusterServiceDNSSuffix: svc.{{ $clusterDomain }}
|
||||
pause: false
|
||||
unmanaged: false
|
||||
image: percona/percona-server-mongodb:{{ include "mongodb.versionMap" $ }}
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
{{- if lt (int .Values.replicas) 3 }}
|
||||
unsafeFlags:
|
||||
replsetSize: true
|
||||
{{- end }}
|
||||
|
||||
updateStrategy: SmartUpdate
|
||||
upgradeOptions:
|
||||
apply: disabled
|
||||
|
||||
pmm:
|
||||
enabled: false
|
||||
image: {{ .Values.images.pmm }}
|
||||
serverHost: ""
|
||||
|
||||
sharding:
|
||||
enabled: {{ .Values.sharding | default false }}
|
||||
balancer:
|
||||
enabled: true
|
||||
{{- if .Values.sharding }}
|
||||
configsvrReplSet:
|
||||
size: {{ .Values.shardingConfig.configServers }}
|
||||
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 8 }}
|
||||
volumeSpec:
|
||||
persistentVolumeClaim:
|
||||
{{- with .Values.storageClass }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.shardingConfig.configServerSize }}
|
||||
affinity:
|
||||
antiAffinityTopologyKey: kubernetes.io/hostname
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: 1
|
||||
mongos:
|
||||
size: {{ .Values.shardingConfig.mongos }}
|
||||
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 8 }}
|
||||
affinity:
|
||||
antiAffinityTopologyKey: kubernetes.io/hostname
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: 1
|
||||
expose:
|
||||
exposeType: ClusterIP
|
||||
{{- end }}
|
||||
|
||||
replsets:
|
||||
{{- if .Values.sharding }}
|
||||
{{- range .Values.shardingConfig.shards }}
|
||||
- name: {{ .name }}
|
||||
size: {{ .replicas }}
|
||||
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list $.Values.resourcesPreset $.Values.resources $) | nindent 8 }}
|
||||
volumeSpec:
|
||||
persistentVolumeClaim:
|
||||
{{- with $.Values.storageClass }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .size }}
|
||||
affinity:
|
||||
antiAffinityTopologyKey: kubernetes.io/hostname
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: 1
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- name: rs0
|
||||
size: {{ .Values.replicas }}
|
||||
resources: {{- include "cozy-lib.resources.defaultingSanitize" (list .Values.resourcesPreset .Values.resources $) | nindent 8 }}
|
||||
volumeSpec:
|
||||
persistentVolumeClaim:
|
||||
{{- with .Values.storageClass }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.size }}
|
||||
affinity:
|
||||
antiAffinityTopologyKey: kubernetes.io/hostname
|
||||
podDisruptionBudget:
|
||||
maxUnavailable: 1
|
||||
expose:
|
||||
enabled: false
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.users }}
|
||||
users:
|
||||
{{- range $username, $user := .Values.users }}
|
||||
{{- if not $user.roles }}
|
||||
{{- fail (printf "users.%s.roles is required and cannot be empty" $username) }}
|
||||
{{- end }}
|
||||
- name: {{ $username }}
|
||||
db: {{ $user.db }}
|
||||
passwordSecretRef:
|
||||
name: {{ $.Release.Name }}-user-{{ $username }}
|
||||
key: password
|
||||
roles:
|
||||
{{- range $user.roles }}
|
||||
- name: {{ .name }}
|
||||
db: {{ .db }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
backup:
|
||||
enabled: {{ .Values.backup.enabled | default false }}
|
||||
image: {{ .Values.images.backup }}
|
||||
{{- if .Values.backup.enabled }}
|
||||
storages:
|
||||
s3-storage:
|
||||
type: s3
|
||||
s3:
|
||||
bucket: {{ .Values.backup.destinationPath | trimPrefix "s3://" | regexFind "^[^/]+" }}
|
||||
prefix: {{ .Values.backup.destinationPath | trimPrefix "s3://" | splitList "/" | rest | join "/" }}
|
||||
endpointUrl: {{ .Values.backup.endpointURL }}
|
||||
credentialsSecret: {{ .Release.Name }}-s3-creds
|
||||
insecureSkipTLSVerify: false
|
||||
forcePathStyle: true
|
||||
tasks:
|
||||
- name: daily-backup
|
||||
enabled: true
|
||||
schedule: {{ .Values.backup.schedule | quote }}
|
||||
keep: {{ .Values.backup.retentionPolicy | trimSuffix "d" | int }}
|
||||
storageName: s3-storage
|
||||
type: logical
|
||||
compressionType: gzip
|
||||
pitr:
|
||||
enabled: true
|
||||
{{- end }}
|
||||
---
|
||||
# WorkloadMonitor tracks data-bearing mongod pods only (not config servers or mongos routers)
|
||||
# The selector filters by component=mongod, so we only count shard replicas
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ .Release.Name }}
|
||||
spec:
|
||||
{{- if .Values.sharding }}
|
||||
{{- $totalReplicas := 0 }}
|
||||
{{- range .Values.shardingConfig.shards }}
|
||||
{{- $totalReplicas = add $totalReplicas .replicas }}
|
||||
{{- end }}
|
||||
replicas: {{ $totalReplicas }}
|
||||
{{- else }}
|
||||
replicas: {{ .Values.replicas }}
|
||||
{{- end }}
|
||||
minReplicas: 1
|
||||
kind: mongodb
|
||||
type: mongodb
|
||||
selector:
|
||||
app.kubernetes.io/name: percona-server-mongodb
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: mongod
|
||||
version: {{ .Chart.Version }}
|
||||
37
packages/apps/mongodb/templates/restore.yaml
Normal file
37
packages/apps/mongodb/templates/restore.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
{{- if .Values.bootstrap.enabled }}
|
||||
{{- if not .Values.bootstrap.backupName }}
|
||||
{{- fail "bootstrap.backupName is required when bootstrap.enabled is true" }}
|
||||
{{- end }}
|
||||
{{- if not .Values.backup.destinationPath }}
|
||||
{{- fail "backup.destinationPath is required when bootstrap.enabled is true" }}
|
||||
{{- end }}
|
||||
{{- if not .Values.backup.endpointURL }}
|
||||
{{- fail "backup.endpointURL is required when bootstrap.enabled is true" }}
|
||||
{{- end }}
|
||||
{{- if not .Values.backup.s3AccessKey }}
|
||||
{{- fail "backup.s3AccessKey is required when bootstrap.enabled is true" }}
|
||||
{{- end }}
|
||||
{{- if not .Values.backup.s3SecretKey }}
|
||||
{{- fail "backup.s3SecretKey is required when bootstrap.enabled is true" }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: psmdb.percona.com/v1
|
||||
kind: PerconaServerMongoDBRestore
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-restore
|
||||
spec:
|
||||
clusterName: {{ .Release.Name }}
|
||||
{{- if .Values.bootstrap.recoveryTime }}
|
||||
pitr:
|
||||
type: date
|
||||
date: {{ .Values.bootstrap.recoveryTime | quote }}
|
||||
{{- end }}
|
||||
backupSource:
|
||||
type: logical
|
||||
destination: {{ .Values.backup.destinationPath | trimSuffix "/" }}/{{ .Values.bootstrap.backupName }}
|
||||
s3:
|
||||
credentialsSecret: {{ .Release.Name }}-s3-creds
|
||||
endpointUrl: {{ .Values.backup.endpointURL }}
|
||||
insecureSkipTLSVerify: false
|
||||
forcePathStyle: true
|
||||
{{- end }}
|
||||
17
packages/apps/mongodb/templates/user-secrets.yaml
Normal file
17
packages/apps/mongodb/templates/user-secrets.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
{{- range $username, $user := .Values.users }}
|
||||
{{- $existingSecret := lookup "v1" "Secret" $.Release.Namespace (printf "%s-user-%s" $.Release.Name $username) }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-user-{{ $username }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- if $user.password }}
|
||||
password: {{ $user.password | quote }}
|
||||
{{- else if and $existingSecret (hasKey $existingSecret.data "password") }}
|
||||
password: {{ index $existingSecret.data "password" | b64dec | quote }}
|
||||
{{- else }}
|
||||
password: {{ randAlphaNum 16 | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
112
packages/apps/mongodb/tests/backup-secret_test.yaml
Normal file
112
packages/apps/mongodb/tests/backup-secret_test.yaml
Normal file
@@ -0,0 +1,112 @@
|
||||
suite: backup secret tests
|
||||
|
||||
templates:
|
||||
- templates/backup-secret.yaml
|
||||
|
||||
tests:
|
||||
# Not rendered when both backup and bootstrap disabled
|
||||
- it: does not render when backup and bootstrap disabled
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
backup:
|
||||
enabled: false
|
||||
bootstrap:
|
||||
enabled: false
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
# Rendered when backup enabled
|
||||
- it: renders when backup enabled
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
backup:
|
||||
enabled: true
|
||||
s3AccessKey: "AKIAIOSFODNN7EXAMPLE"
|
||||
s3SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: Secret
|
||||
|
||||
# Rendered when bootstrap enabled (for restore)
|
||||
- it: renders when bootstrap enabled
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
backup:
|
||||
enabled: false
|
||||
s3AccessKey: "AKIAIOSFODNN7EXAMPLE"
|
||||
s3SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
bootstrap:
|
||||
enabled: true
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
|
||||
# Secret name
|
||||
- it: uses correct secret name
|
||||
release:
|
||||
name: mydb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
backup:
|
||||
enabled: true
|
||||
s3AccessKey: "accesskey"
|
||||
s3SecretKey: "secretkey"
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: mydb-s3-creds
|
||||
|
||||
# Contains AWS credentials
|
||||
- it: contains AWS credentials
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
backup:
|
||||
enabled: true
|
||||
s3AccessKey: "MYACCESSKEY"
|
||||
s3SecretKey: "MYSECRETKEY"
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.AWS_ACCESS_KEY_ID
|
||||
value: "MYACCESSKEY"
|
||||
- equal:
|
||||
path: stringData.AWS_SECRET_ACCESS_KEY
|
||||
value: "MYSECRETKEY"
|
||||
|
||||
# Fails without s3AccessKey
|
||||
- it: fails when s3AccessKey missing
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
backup:
|
||||
enabled: true
|
||||
s3AccessKey: ""
|
||||
s3SecretKey: "secretkey"
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "backup.s3AccessKey is required when backup or bootstrap is enabled"
|
||||
|
||||
# Fails without s3SecretKey
|
||||
- it: fails when s3SecretKey missing
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
backup:
|
||||
enabled: true
|
||||
s3AccessKey: "accesskey"
|
||||
s3SecretKey: ""
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "backup.s3SecretKey is required when backup or bootstrap is enabled"
|
||||
132
packages/apps/mongodb/tests/credentials_test.yaml
Normal file
132
packages/apps/mongodb/tests/credentials_test.yaml
Normal file
@@ -0,0 +1,132 @@
|
||||
suite: credentials tests
|
||||
|
||||
templates:
|
||||
- templates/credentials.yaml
|
||||
|
||||
tests:
|
||||
# Basic rendering
|
||||
- it: always renders a Secret
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: Secret
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: test-mongodb-credentials
|
||||
- equal:
|
||||
path: type
|
||||
value: Opaque
|
||||
|
||||
# Username is always databaseAdmin
|
||||
- it: sets username to databaseAdmin
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.username
|
||||
value: databaseAdmin
|
||||
|
||||
# Port is always 27017
|
||||
- it: sets port to 27017
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.port
|
||||
value: "27017"
|
||||
|
||||
# Host for replica set mode
|
||||
- it: uses rs0 service for replica set mode
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: false
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.host
|
||||
value: test-mongodb-rs0.tenant-test.svc.cozy.local
|
||||
|
||||
# Host for sharded mode
|
||||
- it: uses mongos service for sharded mode
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.host
|
||||
value: test-mongodb-mongos.tenant-test.svc.cozy.local
|
||||
|
||||
# Custom cluster domain
|
||||
- it: uses custom cluster domain
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: custom.domain
|
||||
sharding: false
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.host
|
||||
value: test-mongodb-rs0.tenant-test.svc.custom.domain
|
||||
|
||||
# Default cluster domain when not set
|
||||
- it: defaults to cozy.local when cluster domain not set
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster: {}
|
||||
sharding: false
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.host
|
||||
value: test-mongodb-rs0.tenant-test.svc.cozy.local
|
||||
|
||||
# Password empty without operator secret (lookup returns nil in tests)
|
||||
- it: has empty password on first install
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.password
|
||||
value: ""
|
||||
|
||||
# URI empty without password
|
||||
- it: has empty uri when password not available
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.uri
|
||||
value: ""
|
||||
106
packages/apps/mongodb/tests/dashboard-resourcemap_test.yaml
Normal file
106
packages/apps/mongodb/tests/dashboard-resourcemap_test.yaml
Normal file
@@ -0,0 +1,106 @@
|
||||
suite: dashboard resourcemap tests
|
||||
|
||||
templates:
|
||||
- templates/dashboard-resourcemap.yaml
|
||||
|
||||
tests:
|
||||
# Always renders Role and RoleBinding
|
||||
- it: renders Role and RoleBinding
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 2
|
||||
- isKind:
|
||||
of: Role
|
||||
documentIndex: 0
|
||||
- isKind:
|
||||
of: RoleBinding
|
||||
documentIndex: 1
|
||||
|
||||
# Role naming
|
||||
- it: uses correct Role name
|
||||
release:
|
||||
name: mydb
|
||||
namespace: tenant-test
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: mydb-dashboard-resources
|
||||
documentIndex: 0
|
||||
|
||||
# RoleBinding naming
|
||||
- it: uses correct RoleBinding name
|
||||
release:
|
||||
name: mydb
|
||||
namespace: tenant-test
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: mydb-dashboard-resources
|
||||
documentIndex: 1
|
||||
|
||||
# Role grants access to services
|
||||
- it: grants access to MongoDB services
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
asserts:
|
||||
- contains:
|
||||
path: rules[0].resourceNames
|
||||
content: test-mongodb-rs0
|
||||
documentIndex: 0
|
||||
- contains:
|
||||
path: rules[0].resourceNames
|
||||
content: test-mongodb-mongos
|
||||
documentIndex: 0
|
||||
- contains:
|
||||
path: rules[0].resourceNames
|
||||
content: test-mongodb-external
|
||||
documentIndex: 0
|
||||
|
||||
# Role grants access to credentials secret
|
||||
- it: grants access to credentials secret
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
asserts:
|
||||
- contains:
|
||||
path: rules[1].resourceNames
|
||||
content: test-mongodb-credentials
|
||||
documentIndex: 0
|
||||
|
||||
# Role grants access to workloadmonitor
|
||||
- it: grants access to WorkloadMonitor
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
asserts:
|
||||
- contains:
|
||||
path: rules[2].resourceNames
|
||||
content: test-mongodb
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: rules[2].apiGroups[0]
|
||||
value: cozystack.io
|
||||
documentIndex: 0
|
||||
|
||||
# RoleBinding references correct Role
|
||||
- it: RoleBinding references correct Role
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
asserts:
|
||||
- equal:
|
||||
path: roleRef.kind
|
||||
value: Role
|
||||
documentIndex: 1
|
||||
- equal:
|
||||
path: roleRef.name
|
||||
value: test-mongodb-dashboard-resources
|
||||
documentIndex: 1
|
||||
- equal:
|
||||
path: roleRef.apiGroup
|
||||
value: rbac.authorization.k8s.io
|
||||
documentIndex: 1
|
||||
154
packages/apps/mongodb/tests/external-svc_test.yaml
Normal file
154
packages/apps/mongodb/tests/external-svc_test.yaml
Normal file
@@ -0,0 +1,154 @@
|
||||
suite: external service tests
|
||||
|
||||
templates:
|
||||
- templates/external-svc.yaml
|
||||
|
||||
tests:
|
||||
###################
|
||||
# Rendering #
|
||||
###################
|
||||
|
||||
- it: does not render when external is false
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: false
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: renders LoadBalancer service when external is true
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: Service
|
||||
|
||||
###################
|
||||
# Service config #
|
||||
###################
|
||||
|
||||
- it: uses correct service name
|
||||
release:
|
||||
name: mydb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: mydb-external
|
||||
|
||||
- it: sets LoadBalancer type
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.type
|
||||
value: LoadBalancer
|
||||
|
||||
- it: sets externalTrafficPolicy to Local
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.externalTrafficPolicy
|
||||
value: Local
|
||||
|
||||
- it: exposes MongoDB port 27017
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.ports[0].name
|
||||
value: mongodb
|
||||
- equal:
|
||||
path: spec.ports[0].port
|
||||
value: 27017
|
||||
|
||||
###########################
|
||||
# Common selector labels #
|
||||
###########################
|
||||
|
||||
- it: sets app.kubernetes.io/name selector
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.selector["app.kubernetes.io/name"]
|
||||
value: percona-server-mongodb
|
||||
|
||||
- it: sets app.kubernetes.io/instance selector
|
||||
release:
|
||||
name: mydb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.selector["app.kubernetes.io/instance"]
|
||||
value: mydb
|
||||
|
||||
###########################
|
||||
# Replica set mode #
|
||||
###########################
|
||||
|
||||
- it: selects mongod for replica set mode
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
sharding: false
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.selector["app.kubernetes.io/component"]
|
||||
value: mongod
|
||||
- equal:
|
||||
path: spec.selector["app.kubernetes.io/replset"]
|
||||
value: rs0
|
||||
|
||||
###########################
|
||||
# Sharded mode #
|
||||
###########################
|
||||
|
||||
- it: selects mongos for sharded mode
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
sharding: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.selector["app.kubernetes.io/component"]
|
||||
value: mongos
|
||||
|
||||
- it: does not set replset selector for sharded mode
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
external: true
|
||||
sharding: true
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.selector["app.kubernetes.io/replset"]
|
||||
703
packages/apps/mongodb/tests/mongodb_test.yaml
Normal file
703
packages/apps/mongodb/tests/mongodb_test.yaml
Normal file
@@ -0,0 +1,703 @@
|
||||
suite: mongodb CR tests
|
||||
|
||||
templates:
|
||||
- templates/mongodb.yaml
|
||||
|
||||
tests:
|
||||
###################
|
||||
# Basic rendering #
|
||||
###################
|
||||
|
||||
- it: renders PerconaServerMongoDB and WorkloadMonitor
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 2
|
||||
- isKind:
|
||||
of: PerconaServerMongoDB
|
||||
documentIndex: 0
|
||||
- isKind:
|
||||
of: WorkloadMonitor
|
||||
documentIndex: 1
|
||||
|
||||
- it: sets correct CR name
|
||||
release:
|
||||
name: my-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: my-mongodb
|
||||
documentIndex: 0
|
||||
|
||||
##################
|
||||
# CR Version #
|
||||
##################
|
||||
|
||||
- it: sets crVersion to 1.21.1
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.crVersion
|
||||
value: "1.21.1"
|
||||
documentIndex: 0
|
||||
|
||||
#####################
|
||||
# Cluster DNS #
|
||||
#####################
|
||||
|
||||
- it: sets clusterServiceDNSSuffix from cluster config
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: custom.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.clusterServiceDNSSuffix
|
||||
value: svc.custom.local
|
||||
documentIndex: 0
|
||||
|
||||
- it: defaults clusterServiceDNSSuffix to cozy.local
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster: {}
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.clusterServiceDNSSuffix
|
||||
value: svc.cozy.local
|
||||
documentIndex: 0
|
||||
|
||||
##################
|
||||
# Unsafe flags #
|
||||
##################
|
||||
|
||||
- it: enables unsafeFlags when replicas is 1
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
replicas: 1
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.unsafeFlags.replsetSize
|
||||
value: true
|
||||
documentIndex: 0
|
||||
|
||||
- it: enables unsafeFlags when replicas is 2
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
replicas: 2
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.unsafeFlags.replsetSize
|
||||
value: true
|
||||
documentIndex: 0
|
||||
|
||||
- it: does not set unsafeFlags when replicas is 3
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
replicas: 3
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.unsafeFlags
|
||||
documentIndex: 0
|
||||
|
||||
- it: does not set unsafeFlags when replicas is 5
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
replicas: 5
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.unsafeFlags
|
||||
documentIndex: 0
|
||||
|
||||
###########################
|
||||
# Replica Set Mode #
|
||||
###########################
|
||||
|
||||
- it: configures replica set rs0 in non-sharded mode
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: false
|
||||
replicas: 3
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.sharding.enabled
|
||||
value: false
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.replsets[0].name
|
||||
value: rs0
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.replsets[0].size
|
||||
value: 3
|
||||
documentIndex: 0
|
||||
|
||||
- it: sets storage size for replica set
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: false
|
||||
size: 20Gi
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replsets[0].volumeSpec.persistentVolumeClaim.resources.requests.storage
|
||||
value: 20Gi
|
||||
documentIndex: 0
|
||||
|
||||
- it: sets storageClass when provided
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: false
|
||||
storageClass: fast-ssd
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replsets[0].volumeSpec.persistentVolumeClaim.storageClassName
|
||||
value: fast-ssd
|
||||
documentIndex: 0
|
||||
|
||||
- it: does not set storageClass when empty
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: false
|
||||
storageClass: ""
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.replsets[0].volumeSpec.persistentVolumeClaim.storageClassName
|
||||
documentIndex: 0
|
||||
|
||||
###########################
|
||||
# Sharded Cluster Mode #
|
||||
###########################
|
||||
|
||||
- it: enables sharding when configured
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: true
|
||||
shardingConfig:
|
||||
configServers: 3
|
||||
configServerSize: 3Gi
|
||||
mongos: 2
|
||||
shards:
|
||||
- name: rs0
|
||||
replicas: 3
|
||||
size: 10Gi
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.sharding.enabled
|
||||
value: true
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.sharding.balancer.enabled
|
||||
value: true
|
||||
documentIndex: 0
|
||||
|
||||
- it: configures config servers
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: true
|
||||
shardingConfig:
|
||||
configServers: 5
|
||||
configServerSize: 5Gi
|
||||
mongos: 2
|
||||
shards:
|
||||
- name: rs0
|
||||
replicas: 3
|
||||
size: 10Gi
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.sharding.configsvrReplSet.size
|
||||
value: 5
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.sharding.configsvrReplSet.volumeSpec.persistentVolumeClaim.resources.requests.storage
|
||||
value: 5Gi
|
||||
documentIndex: 0
|
||||
|
||||
- it: configures mongos routers
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: true
|
||||
shardingConfig:
|
||||
configServers: 3
|
||||
configServerSize: 3Gi
|
||||
mongos: 4
|
||||
shards:
|
||||
- name: rs0
|
||||
replicas: 3
|
||||
size: 10Gi
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.sharding.mongos.size
|
||||
value: 4
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.sharding.mongos.expose.exposeType
|
||||
value: ClusterIP
|
||||
documentIndex: 0
|
||||
|
||||
- it: configures multiple shards
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: true
|
||||
shardingConfig:
|
||||
configServers: 3
|
||||
configServerSize: 3Gi
|
||||
mongos: 2
|
||||
shards:
|
||||
- name: shard1
|
||||
replicas: 3
|
||||
size: 50Gi
|
||||
- name: shard2
|
||||
replicas: 5
|
||||
size: 100Gi
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replsets[0].name
|
||||
value: shard1
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.replsets[0].size
|
||||
value: 3
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.replsets[0].volumeSpec.persistentVolumeClaim.resources.requests.storage
|
||||
value: 50Gi
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.replsets[1].name
|
||||
value: shard2
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.replsets[1].size
|
||||
value: 5
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.replsets[1].volumeSpec.persistentVolumeClaim.resources.requests.storage
|
||||
value: 100Gi
|
||||
documentIndex: 0
|
||||
|
||||
###########################
|
||||
# Users configuration #
|
||||
###########################
|
||||
|
||||
- it: does not include users section when no users defined
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
users: {}
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.users
|
||||
documentIndex: 0
|
||||
|
||||
- it: configures users when defined
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
users:
|
||||
appuser:
|
||||
db: appdb
|
||||
roles:
|
||||
- name: readWrite
|
||||
db: appdb
|
||||
asserts:
|
||||
- exists:
|
||||
path: spec.users
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.users[0].name
|
||||
value: appuser
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.users[0].db
|
||||
value: appdb
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.users[0].passwordSecretRef.name
|
||||
value: test-mongodb-user-appuser
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.users[0].passwordSecretRef.key
|
||||
value: password
|
||||
documentIndex: 0
|
||||
|
||||
- it: configures user roles
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
users:
|
||||
admin:
|
||||
db: admin
|
||||
roles:
|
||||
- name: clusterAdmin
|
||||
db: admin
|
||||
- name: userAdminAnyDatabase
|
||||
db: admin
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.users[0].roles[0].name
|
||||
value: clusterAdmin
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.users[0].roles[0].db
|
||||
value: admin
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.users[0].roles[1].name
|
||||
value: userAdminAnyDatabase
|
||||
documentIndex: 0
|
||||
|
||||
- it: fails when user has empty roles
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
users:
|
||||
myuser:
|
||||
db: mydb
|
||||
roles: []
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "users.myuser.roles is required and cannot be empty"
|
||||
|
||||
###########################
|
||||
# Backup configuration #
|
||||
###########################
|
||||
|
||||
- it: disables backup when not enabled
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
backup:
|
||||
enabled: false
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backup.enabled
|
||||
value: false
|
||||
documentIndex: 0
|
||||
- notExists:
|
||||
path: spec.backup.storages
|
||||
documentIndex: 0
|
||||
|
||||
- it: configures backup when enabled
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
backup:
|
||||
enabled: true
|
||||
schedule: "0 3 * * *"
|
||||
retentionPolicy: 14d
|
||||
destinationPath: "s3://mybucket/backups/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backup.enabled
|
||||
value: true
|
||||
documentIndex: 0
|
||||
- equal:
|
||||
path: spec.backup.storages.s3-storage.type
|
||||
value: s3
|
||||
documentIndex: 0
|
||||
|
||||
- it: parses bucket from destinationPath
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
backup:
|
||||
enabled: true
|
||||
destinationPath: "s3://my-backup-bucket/mongodb/prod/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backup.storages.s3-storage.s3.bucket
|
||||
value: my-backup-bucket
|
||||
documentIndex: 0
|
||||
|
||||
- it: parses prefix from destinationPath
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
backup:
|
||||
enabled: true
|
||||
destinationPath: "s3://bucket/path/to/backups/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backup.storages.s3-storage.s3.prefix
|
||||
value: path/to/backups/
|
||||
documentIndex: 0
|
||||
|
||||
- it: sets backup retention from retentionPolicy
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
backup:
|
||||
enabled: true
|
||||
retentionPolicy: 30d
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backup.tasks[0].keep
|
||||
value: 30
|
||||
documentIndex: 0
|
||||
|
||||
- it: sets backup schedule
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
backup:
|
||||
enabled: true
|
||||
schedule: "0 4 * * *"
|
||||
retentionPolicy: 7d
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backup.tasks[0].schedule
|
||||
value: "0 4 * * *"
|
||||
documentIndex: 0
|
||||
|
||||
- it: enables PITR when backup enabled
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
backup:
|
||||
enabled: true
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backup.pitr.enabled
|
||||
value: true
|
||||
documentIndex: 0
|
||||
|
||||
- it: references s3-creds secret for backup
|
||||
release:
|
||||
name: mydb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
backup:
|
||||
enabled: true
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backup.storages.s3-storage.s3.credentialsSecret
|
||||
value: mydb-s3-creds
|
||||
documentIndex: 0
|
||||
|
||||
###########################
|
||||
# WorkloadMonitor #
|
||||
###########################
|
||||
|
||||
- it: creates WorkloadMonitor with correct metadata
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: test-mongodb
|
||||
documentIndex: 1
|
||||
- equal:
|
||||
path: spec.kind
|
||||
value: mongodb
|
||||
documentIndex: 1
|
||||
- equal:
|
||||
path: spec.type
|
||||
value: mongodb
|
||||
documentIndex: 1
|
||||
|
||||
- it: sets replicas from values in non-sharded mode
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: false
|
||||
replicas: 5
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replicas
|
||||
value: 5
|
||||
documentIndex: 1
|
||||
|
||||
- it: calculates total replicas in sharded mode
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
sharding: true
|
||||
shardingConfig:
|
||||
configServers: 3
|
||||
configServerSize: 3Gi
|
||||
mongos: 2
|
||||
shards:
|
||||
- name: rs0
|
||||
replicas: 3
|
||||
size: 10Gi
|
||||
- name: rs1
|
||||
replicas: 5
|
||||
size: 10Gi
|
||||
- name: rs2
|
||||
replicas: 2
|
||||
size: 10Gi
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replicas
|
||||
value: 10
|
||||
documentIndex: 1
|
||||
|
||||
- it: sets minReplicas to 1
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.minReplicas
|
||||
value: 1
|
||||
documentIndex: 1
|
||||
|
||||
- it: sets correct selector labels
|
||||
release:
|
||||
name: mydb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
_cluster:
|
||||
cluster-domain: cozy.local
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.selector["app.kubernetes.io/name"]
|
||||
value: percona-server-mongodb
|
||||
documentIndex: 1
|
||||
- equal:
|
||||
path: spec.selector["app.kubernetes.io/instance"]
|
||||
value: mydb
|
||||
documentIndex: 1
|
||||
- equal:
|
||||
path: spec.selector["app.kubernetes.io/component"]
|
||||
value: mongod
|
||||
documentIndex: 1
|
||||
|
||||
349
packages/apps/mongodb/tests/restore_test.yaml
Normal file
349
packages/apps/mongodb/tests/restore_test.yaml
Normal file
@@ -0,0 +1,349 @@
|
||||
suite: restore tests
|
||||
|
||||
templates:
|
||||
- templates/restore.yaml
|
||||
|
||||
tests:
|
||||
#####################
|
||||
# Rendering #
|
||||
#####################
|
||||
|
||||
- it: does not render when bootstrap is disabled
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: false
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: renders PerconaServerMongoDBRestore CR when enabled
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "my-backup-2025-01-07"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/backups/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: PerconaServerMongoDBRestore
|
||||
|
||||
#####################
|
||||
# Validation #
|
||||
#####################
|
||||
|
||||
- it: fails when backupName is missing
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: ""
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "bootstrap.backupName is required when bootstrap.enabled is true"
|
||||
|
||||
- it: fails when destinationPath is missing
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "my-backup"
|
||||
backup:
|
||||
destinationPath: ""
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "backup.destinationPath is required when bootstrap.enabled is true"
|
||||
|
||||
- it: fails when endpointURL is missing
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "my-backup"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: ""
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "backup.endpointURL is required when bootstrap.enabled is true"
|
||||
|
||||
#####################
|
||||
# CR metadata #
|
||||
#####################
|
||||
|
||||
- it: uses correct restore CR name
|
||||
release:
|
||||
name: mydb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup-2025"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/backups/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: mydb-restore
|
||||
|
||||
- it: references correct cluster name
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup-2025"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/backups/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.clusterName
|
||||
value: test-mongodb
|
||||
|
||||
#####################
|
||||
# Backup source #
|
||||
#####################
|
||||
|
||||
- it: sets backupSource type to logical
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup-2025"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/backups/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backupSource.type
|
||||
value: logical
|
||||
|
||||
- it: constructs destination from destinationPath and backupName
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "daily-backup-2025-01-07"
|
||||
backup:
|
||||
destinationPath: "s3://mybucket/mongodb/prod/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backupSource.destination
|
||||
value: s3://mybucket/mongodb/prod/daily-backup-2025-01-07
|
||||
|
||||
- it: trims trailing slash from destinationPath
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backupSource.destination
|
||||
value: s3://bucket/path/backup
|
||||
|
||||
#####################
|
||||
# S3 configuration #
|
||||
#####################
|
||||
|
||||
- it: references s3-creds secret
|
||||
release:
|
||||
name: mydb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backupSource.s3.credentialsSecret
|
||||
value: mydb-s3-creds
|
||||
|
||||
- it: sets S3 endpoint URL
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "https://s3.amazonaws.com"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backupSource.s3.endpointUrl
|
||||
value: "https://s3.amazonaws.com"
|
||||
|
||||
- it: disables insecureSkipTLSVerify
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backupSource.s3.insecureSkipTLSVerify
|
||||
value: false
|
||||
|
||||
- it: enables forcePathStyle
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.backupSource.s3.forcePathStyle
|
||||
value: true
|
||||
|
||||
#####################
|
||||
# PITR #
|
||||
#####################
|
||||
|
||||
- it: does not set pitr when recoveryTime not specified
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.pitr
|
||||
|
||||
- it: configures PITR when recoveryTime is set
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "my-backup"
|
||||
recoveryTime: "2025-01-07 14:30:00"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/backups/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.pitr.type
|
||||
value: date
|
||||
- equal:
|
||||
path: spec.pitr.date
|
||||
value: "2025-01-07 14:30:00"
|
||||
|
||||
#####################
|
||||
# S3 credentials #
|
||||
#####################
|
||||
|
||||
- it: fails when s3AccessKey is missing
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: ""
|
||||
s3SecretKey: "secret"
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "backup.s3AccessKey is required when bootstrap.enabled is true"
|
||||
|
||||
- it: fails when s3SecretKey is missing
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
bootstrap:
|
||||
enabled: true
|
||||
backupName: "backup"
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/"
|
||||
endpointURL: "http://minio:9000"
|
||||
s3AccessKey: "access"
|
||||
s3SecretKey: ""
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "backup.s3SecretKey is required when bootstrap.enabled is true"
|
||||
98
packages/apps/mongodb/tests/user-secrets_test.yaml
Normal file
98
packages/apps/mongodb/tests/user-secrets_test.yaml
Normal file
@@ -0,0 +1,98 @@
|
||||
suite: user secrets tests
|
||||
|
||||
templates:
|
||||
- templates/user-secrets.yaml
|
||||
|
||||
tests:
|
||||
# No users configured
|
||||
- it: does not render when no users defined
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
users: {}
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
# Single user
|
||||
- it: creates secret for single user
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
users:
|
||||
myuser:
|
||||
db: mydb
|
||||
roles:
|
||||
- name: readWrite
|
||||
db: mydb
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 1
|
||||
- isKind:
|
||||
of: Secret
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: test-mongodb-user-myuser
|
||||
- equal:
|
||||
path: type
|
||||
value: Opaque
|
||||
- exists:
|
||||
path: stringData.password
|
||||
|
||||
# Multiple users
|
||||
- it: creates separate secrets for multiple users
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
users:
|
||||
user1:
|
||||
db: db1
|
||||
roles:
|
||||
- name: readWrite
|
||||
db: db1
|
||||
user2:
|
||||
db: db2
|
||||
roles:
|
||||
- name: dbAdmin
|
||||
db: db2
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 2
|
||||
|
||||
# User with explicit password
|
||||
- it: uses explicit password when provided
|
||||
release:
|
||||
name: test-mongodb
|
||||
namespace: tenant-test
|
||||
set:
|
||||
users:
|
||||
myuser:
|
||||
password: "mysecretpassword"
|
||||
db: mydb
|
||||
roles:
|
||||
- name: readWrite
|
||||
db: mydb
|
||||
asserts:
|
||||
- equal:
|
||||
path: stringData.password
|
||||
value: "mysecretpassword"
|
||||
|
||||
# Secret naming convention
|
||||
- it: follows naming convention release-user-username
|
||||
release:
|
||||
name: prod-db
|
||||
namespace: tenant-prod
|
||||
set:
|
||||
users:
|
||||
admin:
|
||||
db: admin
|
||||
roles:
|
||||
- name: clusterAdmin
|
||||
db: admin
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: prod-db-user-admin
|
||||
309
packages/apps/mongodb/values.schema.json
Normal file
309
packages/apps/mongodb/values.schema.json
Normal file
@@ -0,0 +1,309 @@
|
||||
{
|
||||
"title": "Chart Values",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"backup": {
|
||||
"description": "Backup configuration.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"destinationPath": {
|
||||
"description": "Destination path for backups (e.g. s3://bucket/path/).",
|
||||
"type": "string",
|
||||
"default": "s3://bucket/path/to/folder/"
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Enable regular backups.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"endpointURL": {
|
||||
"description": "S3 endpoint URL for uploads.",
|
||||
"type": "string",
|
||||
"default": "http://minio-gateway-service:9000"
|
||||
},
|
||||
"retentionPolicy": {
|
||||
"description": "Retention policy (e.g. \"30d\").",
|
||||
"type": "string",
|
||||
"default": "30d"
|
||||
},
|
||||
"s3AccessKey": {
|
||||
"description": "Access key for S3 authentication.",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"s3SecretKey": {
|
||||
"description": "Secret key for S3 authentication.",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"schedule": {
|
||||
"description": "Cron schedule for automated backups.",
|
||||
"type": "string",
|
||||
"default": "0 2 * * *"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bootstrap": {
|
||||
"description": "Bootstrap configuration.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"required": [
|
||||
"backupName",
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"backupName": {
|
||||
"description": "Name of backup to restore from.",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Whether to restore from a backup.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"recoveryTime": {
|
||||
"description": "Timestamp for point-in-time recovery; empty means latest.",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"external": {
|
||||
"description": "Enable external access from outside the cluster.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"images": {
|
||||
"description": "Container images used by the operator.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"required": [
|
||||
"backup",
|
||||
"pmm"
|
||||
],
|
||||
"properties": {
|
||||
"backup": {
|
||||
"description": "Percona Backup for MongoDB image.",
|
||||
"type": "string",
|
||||
"default": "percona/percona-backup-mongodb:2.11.0"
|
||||
},
|
||||
"pmm": {
|
||||
"description": "PMM client image for monitoring.",
|
||||
"type": "string",
|
||||
"default": "percona/pmm-client:2.44.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"replicas": {
|
||||
"description": "Number of MongoDB replicas in replica set.",
|
||||
"type": "integer",
|
||||
"default": 3
|
||||
},
|
||||
"resources": {
|
||||
"description": "Explicit CPU and memory configuration for each MongoDB replica. When omitted, the preset defined in `resourcesPreset` is applied.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"description": "CPU available to each replica.",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
},
|
||||
"memory": {
|
||||
"description": "Memory (RAM) available to each replica.",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"resourcesPreset": {
|
||||
"description": "Default sizing preset used when `resources` is omitted.",
|
||||
"type": "string",
|
||||
"default": "small",
|
||||
"enum": [
|
||||
"nano",
|
||||
"micro",
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"xlarge",
|
||||
"2xlarge"
|
||||
]
|
||||
},
|
||||
"sharding": {
|
||||
"description": "Enable sharded cluster mode. When disabled, deploys a replica set.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"shardingConfig": {
|
||||
"description": "Configuration for sharded cluster mode.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"required": [
|
||||
"configServerSize",
|
||||
"configServers",
|
||||
"mongos"
|
||||
],
|
||||
"properties": {
|
||||
"configServerSize": {
|
||||
"description": "PVC size for config servers.",
|
||||
"default": "3Gi",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
},
|
||||
"configServers": {
|
||||
"description": "Number of config server replicas.",
|
||||
"type": "integer",
|
||||
"default": 3
|
||||
},
|
||||
"mongos": {
|
||||
"description": "Number of mongos router replicas.",
|
||||
"type": "integer",
|
||||
"default": 2
|
||||
},
|
||||
"shards": {
|
||||
"description": "List of shard configurations.",
|
||||
"type": "array",
|
||||
"default": [
|
||||
{
|
||||
"name": "rs0",
|
||||
"replicas": 3,
|
||||
"size": "10Gi"
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"replicas",
|
||||
"size"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Shard name.",
|
||||
"type": "string"
|
||||
},
|
||||
"replicas": {
|
||||
"description": "Number of replicas in this shard.",
|
||||
"type": "integer"
|
||||
},
|
||||
"size": {
|
||||
"description": "PVC size for this shard.",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"description": "Persistent Volume Claim size available for application data.",
|
||||
"default": "10Gi",
|
||||
"pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"x-kubernetes-int-or-string": true
|
||||
},
|
||||
"storageClass": {
|
||||
"description": "StorageClass used to store the data.",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"users": {
|
||||
"description": "Custom MongoDB users configuration map.",
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"db"
|
||||
],
|
||||
"properties": {
|
||||
"db": {
|
||||
"description": "Database to authenticate against.",
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"description": "Password for the user (auto-generated if omitted).",
|
||||
"type": "string"
|
||||
},
|
||||
"roles": {
|
||||
"description": "List of MongoDB roles with database scope.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"db",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"db": {
|
||||
"description": "Database the role applies to.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Role name (e.g., readWrite, dbAdmin, clusterAdmin).",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"description": "MongoDB major version to deploy.",
|
||||
"type": "string",
|
||||
"default": "v8",
|
||||
"enum": [
|
||||
"v8",
|
||||
"v7",
|
||||
"v6"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
147
packages/apps/mongodb/values.yaml
Normal file
147
packages/apps/mongodb/values.yaml
Normal file
@@ -0,0 +1,147 @@
|
||||
##
|
||||
## @section Common parameters
|
||||
##
|
||||
|
||||
## @typedef {struct} Resources - Explicit CPU and memory configuration for each MongoDB replica.
|
||||
## @field {quantity} [cpu] - CPU available to each replica.
|
||||
## @field {quantity} [memory] - Memory (RAM) available to each replica.
|
||||
|
||||
## @enum {string} ResourcesPreset - Default sizing preset.
|
||||
## @value nano
|
||||
## @value micro
|
||||
## @value small
|
||||
## @value medium
|
||||
## @value large
|
||||
## @value xlarge
|
||||
## @value 2xlarge
|
||||
|
||||
## @param {int} replicas - Number of MongoDB replicas in replica set.
|
||||
replicas: 3
|
||||
|
||||
## @param {Resources} [resources] - Explicit CPU and memory configuration for each MongoDB replica. When omitted, the preset defined in `resourcesPreset` is applied.
|
||||
resources: {}
|
||||
|
||||
## @param {ResourcesPreset} resourcesPreset="small" - Default sizing preset used when `resources` is omitted.
|
||||
resourcesPreset: "small"
|
||||
|
||||
## @param {quantity} size - Persistent Volume Claim size available for application data.
|
||||
size: 10Gi
|
||||
|
||||
## @param {string} storageClass - StorageClass used to store the data.
|
||||
storageClass: ""
|
||||
|
||||
## @param {bool} external - Enable external access from outside the cluster.
|
||||
external: false
|
||||
|
||||
##
|
||||
## @enum {string} Version
|
||||
## @value v8
|
||||
## @value v7
|
||||
## @value v6
|
||||
|
||||
## @param {Version} version - MongoDB major version to deploy.
|
||||
version: v8
|
||||
|
||||
##
|
||||
## @section Image configuration
|
||||
##
|
||||
|
||||
## @typedef {struct} Images - Container image configuration.
|
||||
## @field {string} pmm - PMM client image for monitoring.
|
||||
## @field {string} backup - Percona Backup for MongoDB image.
|
||||
|
||||
## @param {Images} images - Container images used by the operator.
|
||||
images:
|
||||
pmm: "percona/pmm-client:2.44.1"
|
||||
backup: "percona/percona-backup-mongodb:2.11.0"
|
||||
|
||||
##
|
||||
## @section Sharding configuration
|
||||
##
|
||||
|
||||
## @param {bool} sharding - Enable sharded cluster mode. When disabled, deploys a replica set.
|
||||
sharding: false
|
||||
|
||||
## @typedef {struct} ShardingConfig - Sharded cluster configuration.
|
||||
## @field {int} configServers - Number of config server replicas.
|
||||
## @field {quantity} configServerSize - PVC size for config servers.
|
||||
## @field {int} mongos - Number of mongos router replicas.
|
||||
## @field {[]Shard} shards - List of shard configurations.
|
||||
|
||||
## @typedef {struct} Shard - Individual shard configuration.
|
||||
## @field {string} name - Shard name.
|
||||
## @field {int} replicas - Number of replicas in this shard.
|
||||
## @field {quantity} size - PVC size for this shard.
|
||||
|
||||
## @param {ShardingConfig} shardingConfig - Configuration for sharded cluster mode.
|
||||
shardingConfig:
|
||||
configServers: 3
|
||||
configServerSize: 3Gi
|
||||
mongos: 2
|
||||
shards:
|
||||
- name: rs0
|
||||
replicas: 3
|
||||
size: 10Gi
|
||||
|
||||
##
|
||||
## @section Users configuration
|
||||
##
|
||||
|
||||
## @typedef {struct} Role - MongoDB role configuration.
|
||||
## @field {string} name - Role name (e.g., readWrite, dbAdmin, clusterAdmin).
|
||||
## @field {string} db - Database the role applies to.
|
||||
|
||||
## @typedef {struct} User - User configuration.
|
||||
## @field {string} [password] - Password for the user (auto-generated if omitted).
|
||||
## @field {string} db - Database to authenticate against.
|
||||
## @field {[]Role} roles - List of MongoDB roles with database scope.
|
||||
|
||||
## @param {map[string]User} users - Custom MongoDB users configuration map.
|
||||
users: {}
|
||||
## Example:
|
||||
## users:
|
||||
## myuser:
|
||||
## db: mydb
|
||||
## roles:
|
||||
## - name: readWrite
|
||||
## db: mydb
|
||||
## - name: dbAdmin
|
||||
## db: mydb
|
||||
|
||||
##
|
||||
## @section Backup parameters
|
||||
##
|
||||
|
||||
## @typedef {struct} Backup - Backup configuration.
|
||||
## @field {bool} enabled - Enable regular backups.
|
||||
## @field {string} [schedule] - Cron schedule for automated backups.
|
||||
## @field {string} [retentionPolicy] - Retention policy (e.g. "30d").
|
||||
## @field {string} [destinationPath] - Destination path for backups (e.g. s3://bucket/path/).
|
||||
## @field {string} [endpointURL] - S3 endpoint URL for uploads.
|
||||
## @field {string} [s3AccessKey] - Access key for S3 authentication.
|
||||
## @field {string} [s3SecretKey] - Secret key for S3 authentication.
|
||||
|
||||
## @param {Backup} backup - Backup configuration.
|
||||
backup:
|
||||
enabled: false
|
||||
schedule: "0 2 * * *"
|
||||
retentionPolicy: 30d
|
||||
destinationPath: "s3://bucket/path/to/folder/"
|
||||
endpointURL: "http://minio-gateway-service:9000"
|
||||
s3AccessKey: ""
|
||||
s3SecretKey: ""
|
||||
|
||||
##
|
||||
## @section Bootstrap (recovery) parameters
|
||||
##
|
||||
|
||||
## @typedef {struct} Bootstrap - Bootstrap configuration for restoring a database cluster from a backup.
|
||||
## @field {bool} enabled - Whether to restore from a backup.
|
||||
## @field {string} [recoveryTime] - Timestamp for point-in-time recovery; empty means latest.
|
||||
## @field {string} backupName - Name of backup to restore from.
|
||||
|
||||
## @param {Bootstrap} bootstrap - Bootstrap configuration.
|
||||
bootstrap:
|
||||
enabled: false
|
||||
recoveryTime: ""
|
||||
backupName: ""
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/mariadb-backup:0.0.0@sha256:1c0beb1b23a109b0e13727b4c73d2c74830e11cede92858ab20101b66f45a858
|
||||
ghcr.io/cozystack/cozystack/mariadb-backup:0.0.0@sha256:aca403030ff5d831415d72367866fdf291fab73ee2cfddbe4c93c2915a316ab1
|
||||
|
||||
@@ -4,4 +4,4 @@ description: Managed RabbitMQ service
|
||||
icon: /logos/rabbitmq.svg
|
||||
type: application
|
||||
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
|
||||
appVersion: "3.13.2"
|
||||
appVersion: "4.2.4"
|
||||
|
||||
@@ -3,3 +3,7 @@ include ../../../scripts/package.mk
|
||||
generate:
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
../../../hack/update-crd.sh
|
||||
|
||||
update:
|
||||
hack/update-versions.sh
|
||||
make generate
|
||||
|
||||
@@ -23,6 +23,7 @@ The service utilizes official RabbitMQ operator. This ensures the reliability an
|
||||
| `size` | Persistent Volume Claim size available for application data. | `quantity` | `10Gi` |
|
||||
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
|
||||
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
|
||||
| `version` | RabbitMQ major.minor version to deploy | `string` | `v4.2` |
|
||||
|
||||
|
||||
### Application-specific parameters
|
||||
|
||||
4
packages/apps/rabbitmq/files/versions.yaml
Normal file
4
packages/apps/rabbitmq/files/versions.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
"v4.2": "4.2.4"
|
||||
"v4.1": "4.1.8"
|
||||
"v4.0": "4.0.9"
|
||||
"v3.13": "3.13.7"
|
||||
129
packages/apps/rabbitmq/hack/update-versions.sh
Executable file
129
packages/apps/rabbitmq/hack/update-versions.sh
Executable file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
RABBITMQ_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
VALUES_FILE="${RABBITMQ_DIR}/values.yaml"
|
||||
VERSIONS_FILE="${RABBITMQ_DIR}/files/versions.yaml"
|
||||
GITHUB_API_URL="https://api.github.com/repos/rabbitmq/rabbitmq-server/releases"
|
||||
|
||||
# Check if jq is installed
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is not installed. Please install jq and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fetch releases from GitHub API
|
||||
echo "Fetching releases from GitHub API..."
|
||||
RELEASES_JSON=$(curl -sSL "${GITHUB_API_URL}?per_page=100")
|
||||
|
||||
if [ -z "$RELEASES_JSON" ]; then
|
||||
echo "Error: Could not fetch releases from GitHub API" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract stable release tags (format: v3.13.7, v4.0.3, etc.)
|
||||
# Filter out pre-releases and draft releases
|
||||
RELEASE_TAGS=$(echo "$RELEASES_JSON" | jq -r '.[] | select(.prerelease == false) | select(.draft == false) | .tag_name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V)
|
||||
|
||||
if [ -z "$RELEASE_TAGS" ]; then
|
||||
echo "Error: Could not find any stable release tags" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found release tags: $(echo "$RELEASE_TAGS" | tr '\n' ' ')"
|
||||
|
||||
# Supported major.minor versions (newest first)
|
||||
# We support the last few minor releases of each active major
|
||||
SUPPORTED_MAJORS=("4.2" "4.1" "4.0" "3.13")
|
||||
|
||||
# Build versions map: major.minor -> latest patch version
|
||||
declare -A VERSION_MAP
|
||||
MAJOR_VERSIONS=()
|
||||
|
||||
for major_minor in "${SUPPORTED_MAJORS[@]}"; do
|
||||
# Find the latest patch version for this major.minor
|
||||
MATCHING=$(echo "$RELEASE_TAGS" | grep -E "^v${major_minor//./\\.}\.[0-9]+$" | tail -n1)
|
||||
|
||||
if [ -n "$MATCHING" ]; then
|
||||
# Strip the 'v' prefix for the value (Docker tag format is e.g. 3.13.7)
|
||||
TAG_VERSION="${MATCHING#v}"
|
||||
VERSION_MAP["v${major_minor}"]="${TAG_VERSION}"
|
||||
MAJOR_VERSIONS+=("v${major_minor}")
|
||||
echo "Found version: v${major_minor} -> ${TAG_VERSION}"
|
||||
else
|
||||
echo "Warning: No stable releases found for ${major_minor}, skipping..." >&2
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MAJOR_VERSIONS[@]} -eq 0 ]; then
|
||||
echo "Error: No matching versions found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Major versions to add: ${MAJOR_VERSIONS[*]}"
|
||||
|
||||
# Create/update versions.yaml file
|
||||
echo "Updating $VERSIONS_FILE..."
|
||||
{
|
||||
for major_ver in "${MAJOR_VERSIONS[@]}"; do
|
||||
echo "\"${major_ver}\": \"${VERSION_MAP[$major_ver]}\""
|
||||
done
|
||||
} > "$VERSIONS_FILE"
|
||||
|
||||
echo "Successfully updated $VERSIONS_FILE"
|
||||
|
||||
# Update values.yaml - enum with major.minor versions only
|
||||
TEMP_FILE=$(mktemp)
|
||||
trap "rm -f $TEMP_FILE" EXIT
|
||||
|
||||
# Build new version section
|
||||
NEW_VERSION_SECTION="## @enum {string} Version"
|
||||
for major_ver in "${MAJOR_VERSIONS[@]}"; do
|
||||
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
|
||||
## @value $major_ver"
|
||||
done
|
||||
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
|
||||
|
||||
## @param {Version} version - RabbitMQ major.minor version to deploy
|
||||
version: ${MAJOR_VERSIONS[0]}"
|
||||
|
||||
# Check if version section already exists
|
||||
if grep -q "^## @enum {string} Version" "$VALUES_FILE"; then
|
||||
# Version section exists, update it using awk
|
||||
echo "Updating existing version section in $VALUES_FILE..."
|
||||
|
||||
awk -v new_section="$NEW_VERSION_SECTION" '
|
||||
/^## @enum {string} Version/ {
|
||||
in_section = 1
|
||||
print new_section
|
||||
next
|
||||
}
|
||||
in_section && /^version: / {
|
||||
in_section = 0
|
||||
next
|
||||
}
|
||||
in_section {
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
|
||||
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
|
||||
else
|
||||
# Version section doesn't exist, insert it before Application-specific parameters section
|
||||
echo "Inserting new version section in $VALUES_FILE..."
|
||||
|
||||
awk -v new_section="$NEW_VERSION_SECTION" '
|
||||
/^## @section Application-specific parameters/ {
|
||||
print new_section
|
||||
print ""
|
||||
}
|
||||
{ print }
|
||||
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
|
||||
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
|
||||
fi
|
||||
|
||||
echo "Successfully updated $VALUES_FILE with major.minor versions: ${MAJOR_VERSIONS[*]}"
|
||||
8
packages/apps/rabbitmq/templates/_versions.tpl
Normal file
8
packages/apps/rabbitmq/templates/_versions.tpl
Normal file
@@ -0,0 +1,8 @@
|
||||
{{- define "rabbitmq.versionMap" }}
|
||||
{{- $versionMap := .Files.Get "files/versions.yaml" | fromYaml }}
|
||||
{{- if not (hasKey $versionMap .Values.version) }}
|
||||
{{- printf `RabbitMQ version %s is not supported, allowed versions are %s` $.Values.version (keys $versionMap) | fail }}
|
||||
{{- end }}
|
||||
{{- index $versionMap .Values.version }}
|
||||
{{- end }}
|
||||
|
||||
@@ -7,6 +7,7 @@ metadata:
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicas }}
|
||||
image: 'rabbitmq:{{ include "rabbitmq.versionMap" $ }}-management'
|
||||
{{- if .Values.external }}
|
||||
service:
|
||||
type: LoadBalancer
|
||||
|
||||
@@ -92,6 +92,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"description": "RabbitMQ major.minor version to deploy",
|
||||
"type": "string",
|
||||
"default": "v4.2",
|
||||
"enum": [
|
||||
"v4.2",
|
||||
"v4.1",
|
||||
"v4.0",
|
||||
"v3.13"
|
||||
]
|
||||
},
|
||||
"vhosts": {
|
||||
"description": "Virtual hosts configuration map.",
|
||||
"type": "object",
|
||||
|
||||
@@ -34,6 +34,15 @@ storageClass: ""
|
||||
external: false
|
||||
|
||||
##
|
||||
## @enum {string} Version
|
||||
## @value v4.2
|
||||
## @value v4.1
|
||||
## @value v4.0
|
||||
## @value v3.13
|
||||
|
||||
## @param {Version} version - RabbitMQ major.minor version to deploy
|
||||
version: v4.2
|
||||
|
||||
## @section Application-specific parameters
|
||||
##
|
||||
|
||||
|
||||
@@ -12,10 +12,7 @@ apply:
|
||||
diff:
|
||||
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f-
|
||||
|
||||
update: update-old update-new
|
||||
|
||||
# TODO: remove old manifest after migration to cozystack-operator
|
||||
update-old:
|
||||
update:
|
||||
timoni bundle build -f flux-aio.cue > templates/fluxcd.yaml
|
||||
yq eval '(select(.kind == "Namespace") | .metadata.labels."pod-security.kubernetes.io/enforce") = "privileged"' -i templates/fluxcd.yaml
|
||||
sed -i templates/fluxcd.yaml \
|
||||
@@ -23,12 +20,3 @@ update-old:
|
||||
-e 's|\.cluster\.local\.,||g' -e 's|\.cluster\.local\,||g' -e 's|\.cluster\.local\.||g' \
|
||||
-e '/value: .svc/a \ {{- include "cozy.kubernetes_envs" . | nindent 12 }}' \
|
||||
-e '/hostNetwork: true/i \ dnsPolicy: ClusterFirstWithHostNet'
|
||||
|
||||
update-new:
|
||||
timoni bundle build -f flux-aio.cue > ../../../internal/fluxinstall/manifests/fluxcd.yaml
|
||||
yq eval '(select(.kind == "Namespace") | .metadata.labels."pod-security.kubernetes.io/enforce") = "privileged"' -i ../../../internal/fluxinstall/manifests/fluxcd.yaml
|
||||
sed -i ../../../internal/fluxinstall/manifests/fluxcd.yaml \
|
||||
-e '/timoni/d' \
|
||||
-e 's|\.cluster\.local\.,||g' -e 's|\.cluster\.local\,||g' -e 's|\.cluster\.local\.||g'
|
||||
# TODO: solve dns issue with hostNetwork for installing helmreleases in tenant k8s clusters
|
||||
#-e '/hostNetwork: true/i \ dnsPolicy: ClusterFirstWithHostNet'
|
||||
|
||||
@@ -7,42 +7,23 @@ pre-checks:
|
||||
../../../hack/pre-checks.sh
|
||||
|
||||
show:
|
||||
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain
|
||||
cozyhr show -n $(NAMESPACE) $(NAME) --plain
|
||||
|
||||
apply:
|
||||
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl apply --filename -
|
||||
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl apply -f-
|
||||
|
||||
diff:
|
||||
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl diff --filename -
|
||||
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f -
|
||||
|
||||
image: pre-checks image-operator image-packages
|
||||
image: pre-checks image-cozystack
|
||||
|
||||
update-version:
|
||||
TAG="$(call settag,$(TAG))" \
|
||||
yq --inplace '.cozystackOperator.cozystackVersion = strenv(TAG)' values.yaml
|
||||
|
||||
image-operator:
|
||||
docker buildx build --file images/cozystack-operator/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/cozystack-operator:$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/cozystack-operator:latest \
|
||||
image-cozystack:
|
||||
docker buildx build -f images/cozystack/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/installer:$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/installer:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/cozystack-operator.json \
|
||||
--metadata-file images/installer.json \
|
||||
$(BUILDX_ARGS)
|
||||
IMAGE="$(REGISTRY)/cozystack-operator:$(call settag,$(TAG))@$$(yq --exit-status '.["containerimage.digest"]' images/cozystack-operator.json --output-format json --raw-output)" \
|
||||
yq --inplace '.cozystackOperator.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/cozystack-operator.json
|
||||
|
||||
image-packages: update-version
|
||||
mkdir -p ../../../_out/assets images
|
||||
flux push artifact \
|
||||
oci://$(REGISTRY)/cozystack-packages:$(call settag,$(TAG)) \
|
||||
--path=../../../packages \
|
||||
--source=https://github.com/cozystack/cozystack \
|
||||
--revision="$$(git describe --tags):$$(git rev-parse HEAD)" \
|
||||
2>&1 | tee images/cozystack-packages.log
|
||||
REPO="oci://$(REGISTRY)/cozystack-packages" \
|
||||
DIGEST=$$(awk --field-separator @ '/artifact successfully pushed/ {print $$2}' images/cozystack-packages.log) && \
|
||||
rm -f images/cozystack-packages.log && \
|
||||
test -n "$$DIGEST" && \
|
||||
yq --inplace '.cozystackOperator.platformSourceUrl = strenv(REPO)' values.yaml && \
|
||||
yq --inplace '.cozystackOperator.platformSourceRef = "digest=" + strenv(DIGEST)' values.yaml
|
||||
IMAGE="$(REGISTRY)/installer:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/installer.json -o json -r)" \
|
||||
yq -i '.cozystack.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/installer.json
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
*.yaml linguist-generated
|
||||
@@ -1,171 +0,0 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.16.4
|
||||
name: packages.cozystack.io
|
||||
spec:
|
||||
group: cozystack.io
|
||||
names:
|
||||
kind: Package
|
||||
listKind: PackageList
|
||||
plural: packages
|
||||
shortNames:
|
||||
- pkg
|
||||
- pkgs
|
||||
singular: package
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: Selected variant
|
||||
jsonPath: .spec.variant
|
||||
name: Variant
|
||||
type: string
|
||||
- description: Ready status
|
||||
jsonPath: .status.conditions[?(@.type=='Ready')].status
|
||||
name: Ready
|
||||
type: string
|
||||
- description: Ready message
|
||||
jsonPath: .status.conditions[?(@.type=='Ready')].message
|
||||
name: Status
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Package is the Schema for the packages API
|
||||
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: PackageSpec defines the desired state of Package
|
||||
properties:
|
||||
components:
|
||||
additionalProperties:
|
||||
description: PackageComponent defines overrides for a specific component
|
||||
properties:
|
||||
enabled:
|
||||
description: |-
|
||||
Enabled indicates whether this component should be installed
|
||||
If false, the component will be disabled even if it's defined in the PackageSource
|
||||
type: boolean
|
||||
values:
|
||||
description: |-
|
||||
Values contains Helm chart values as a JSON object
|
||||
These values will be merged with the default values from the PackageSource
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
type: object
|
||||
description: |-
|
||||
Components is a map of release name to component overrides
|
||||
Allows overriding values and enabling/disabling specific components from the PackageSource
|
||||
type: object
|
||||
ignoreDependencies:
|
||||
description: |-
|
||||
IgnoreDependencies is a list of package source dependencies to ignore
|
||||
Dependencies listed here will not be installed even if they are specified in the PackageSource
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
variant:
|
||||
description: |-
|
||||
Variant is the name of the variant to use from the PackageSource
|
||||
If not specified, defaults to "default"
|
||||
type: string
|
||||
type: object
|
||||
status:
|
||||
description: PackageStatus defines the observed state of Package
|
||||
properties:
|
||||
conditions:
|
||||
description: Conditions represents the latest available observations
|
||||
of a Package'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
|
||||
dependencies:
|
||||
additionalProperties:
|
||||
description: DependencyStatus represents the readiness status of
|
||||
a dependency
|
||||
properties:
|
||||
ready:
|
||||
description: Ready indicates whether the dependency is ready
|
||||
type: boolean
|
||||
required:
|
||||
- ready
|
||||
type: object
|
||||
description: |-
|
||||
Dependencies tracks the readiness status of each dependency
|
||||
Key is the dependency package name, value indicates if the dependency is ready
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -1,250 +0,0 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.16.4
|
||||
name: packagesources.cozystack.io
|
||||
spec:
|
||||
group: cozystack.io
|
||||
names:
|
||||
kind: PackageSource
|
||||
listKind: PackageSourceList
|
||||
plural: packagesources
|
||||
shortNames:
|
||||
- pks
|
||||
singular: packagesource
|
||||
scope: Cluster
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: Package variants (comma-separated)
|
||||
jsonPath: .status.variants
|
||||
name: Variants
|
||||
type: string
|
||||
- description: Ready status
|
||||
jsonPath: .status.conditions[?(@.type=='Ready')].status
|
||||
name: Ready
|
||||
type: string
|
||||
- description: Ready message
|
||||
jsonPath: .status.conditions[?(@.type=='Ready')].message
|
||||
name: Status
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: PackageSource is the Schema for the packagesources API
|
||||
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: PackageSourceSpec defines the desired state of PackageSource
|
||||
properties:
|
||||
sourceRef:
|
||||
description: SourceRef is the source reference for the package source
|
||||
charts
|
||||
properties:
|
||||
kind:
|
||||
description: Kind of the source reference
|
||||
enum:
|
||||
- GitRepository
|
||||
- OCIRepository
|
||||
type: string
|
||||
name:
|
||||
description: Name of the source reference
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the source reference
|
||||
type: string
|
||||
path:
|
||||
description: |-
|
||||
Path is the base path where packages are located in the source.
|
||||
For GitRepository, defaults to "packages" if not specified.
|
||||
For OCIRepository, defaults to empty string (root) if not specified.
|
||||
type: string
|
||||
required:
|
||||
- kind
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
variants:
|
||||
description: |-
|
||||
Variants is a list of package source variants
|
||||
Each variant defines components, applications, dependencies, and libraries for a specific configuration
|
||||
items:
|
||||
description: Variant defines a single variant configuration
|
||||
properties:
|
||||
components:
|
||||
description: Components is a list of Helm releases to be installed
|
||||
as part of this variant
|
||||
items:
|
||||
description: Component defines a single Helm release component
|
||||
within a package source
|
||||
properties:
|
||||
install:
|
||||
description: Install defines installation parameters for
|
||||
this component
|
||||
properties:
|
||||
dependsOn:
|
||||
description: DependsOn is a list of component names
|
||||
that must be installed before this component
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
namespace:
|
||||
description: Namespace is the Kubernetes namespace
|
||||
where the release will be installed
|
||||
type: string
|
||||
privileged:
|
||||
description: Privileged indicates whether this release
|
||||
requires privileged access
|
||||
type: boolean
|
||||
releaseName:
|
||||
description: |-
|
||||
ReleaseName is the name of the HelmRelease resource that will be created
|
||||
If not specified, defaults to the component Name field
|
||||
type: string
|
||||
type: object
|
||||
libraries:
|
||||
description: |-
|
||||
Libraries is a list of library names that this component depends on
|
||||
These libraries must be defined at the variant level
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
name:
|
||||
description: Name is the unique identifier for this component
|
||||
within the package source
|
||||
type: string
|
||||
path:
|
||||
description: Path is the path to the Helm chart directory
|
||||
type: string
|
||||
valuesFiles:
|
||||
description: ValuesFiles is a list of values file names
|
||||
to use
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
- path
|
||||
type: object
|
||||
type: array
|
||||
dependsOn:
|
||||
description: |-
|
||||
DependsOn is a list of package source dependencies
|
||||
For example: "cozystack.networking"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
libraries:
|
||||
description: Libraries is a list of Helm library charts used
|
||||
by components in this variant
|
||||
items:
|
||||
description: Library defines a Helm library chart
|
||||
properties:
|
||||
name:
|
||||
description: Name is the optional name for library placed
|
||||
in charts
|
||||
type: string
|
||||
path:
|
||||
description: Path is the path to the library chart directory
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
type: object
|
||||
type: array
|
||||
name:
|
||||
description: Name is the unique identifier for this variant
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
status:
|
||||
description: PackageSourceStatus defines the observed state of PackageSource
|
||||
properties:
|
||||
conditions:
|
||||
description: Conditions represents the latest available observations
|
||||
of a PackageSource'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
|
||||
variants:
|
||||
description: |-
|
||||
Variants is a comma-separated list of package variant names
|
||||
This field is populated by the controller based on spec.variants keys
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: Package
|
||||
metadata:
|
||||
name: cozystack.cozystack-platform
|
||||
spec:
|
||||
variant: isp-full
|
||||
components:
|
||||
platform:
|
||||
values:
|
||||
publishing:
|
||||
host: "dev5.infra.aenix.org"
|
||||
apiServerEndpoint: "https://api.dev5.infra.aenix.org"
|
||||
externalIPs:
|
||||
- 10.4.0.94
|
||||
- 10.4.0.179
|
||||
- 10.4.0.26
|
||||
authentication:
|
||||
oidc:
|
||||
enabled: true
|
||||
@@ -1,23 +0,0 @@
|
||||
FROM golang:1.25-alpine as builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
|
||||
COPY . /src/
|
||||
WORKDIR /src
|
||||
|
||||
RUN go mod download
|
||||
|
||||
# Build cozystack-operator
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||
-ldflags="-w -s" \
|
||||
-o /cozystack-operator \
|
||||
./cmd/cozystack-operator
|
||||
|
||||
FROM alpine:3.22
|
||||
|
||||
COPY --from=builder /cozystack-operator /usr/bin/cozystack-operator
|
||||
|
||||
ENTRYPOINT ["/usr/bin/cozystack-operator"]
|
||||
@@ -1,12 +0,0 @@
|
||||
# Exclude everything except src directory
|
||||
*
|
||||
!src/**
|
||||
!api/**
|
||||
!cmd/**
|
||||
!hack/**
|
||||
!internal/**
|
||||
!packages/**
|
||||
!pkg/**
|
||||
!scripts/**
|
||||
!go.mod
|
||||
!go.sum
|
||||
41
packages/core/installer/images/cozystack/Dockerfile
Normal file
41
packages/core/installer/images/cozystack/Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
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
|
||||
|
||||
# TARGETARCH is a docker special variable: https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk add --no-cache git make
|
||||
RUN git clone ${K8S_AWAIT_ELECTION_GITREPO} /usr/local/go/k8s-await-election/ \
|
||||
&& cd /usr/local/go/k8s-await-election \
|
||||
&& git reset --hard v${K8S_AWAIT_ELECTION_VERSION} \
|
||||
&& make \
|
||||
&& mv ./out/k8s-await-election-${TARGETARCH} /k8s-await-election
|
||||
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
RUN apk add helm --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community
|
||||
|
||||
COPY . /src/
|
||||
WORKDIR /src
|
||||
|
||||
RUN go mod download
|
||||
|
||||
FROM alpine:3.22
|
||||
|
||||
RUN wget -O- https://github.com/cozystack/cozyhr/raw/refs/heads/main/hack/install.sh | sh -s -- -v 1.5.0
|
||||
|
||||
RUN apk add --no-cache make kubectl helm coreutils git jq openssl
|
||||
|
||||
COPY --from=builder /src/scripts /cozystack/scripts
|
||||
COPY --from=builder /src/packages/core /cozystack/packages/core
|
||||
COPY --from=builder /src/packages/system /cozystack/packages/system
|
||||
COPY --from=k8s-await-election-builder /k8s-await-election /usr/bin/k8s-await-election
|
||||
|
||||
WORKDIR /cozystack
|
||||
ENTRYPOINT ["/usr/bin/k8s-await-election", "/cozystack/scripts/installer.sh" ]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user