Skip to content

Commit 8989791

Browse files
authored
[backups] Build and deploy backup controller (#1685)
## What this PR does This patch adds compilation and docker build steps for the backup controller as well as adding a Helm chart to deploy it as part of the PaaS bundles. ### Release note ```release-note [backups] Build and deploy backup controller ```
2 parents 67ecf3d + 8928552 commit 8989791

14 files changed

Lines changed: 346 additions & 0 deletions

File tree

‎Makefile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ build: build-deps
1515
make -C packages/extra/monitoring image
1616
make -C packages/system/cozystack-api image
1717
make -C packages/system/cozystack-controller image
18+
make -C packages/system/backup-controller image
1819
make -C packages/system/lineage-controller-webhook image
1920
make -C packages/system/cilium image
2021
make -C packages/system/kubeovn-webhook image

‎cmd/backup-controller/main.go‎

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"crypto/tls"
21+
"flag"
22+
"os"
23+
24+
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
25+
// to ensure that exec-entrypoint and run can make use of them.
26+
_ "k8s.io/client-go/plugin/pkg/client/auth"
27+
28+
"k8s.io/apimachinery/pkg/runtime"
29+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
30+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
31+
ctrl "sigs.k8s.io/controller-runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/healthz"
33+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
34+
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
35+
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
36+
"sigs.k8s.io/controller-runtime/pkg/webhook"
37+
38+
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
39+
"github.com/cozystack/cozystack/internal/backupcontroller"
40+
// +kubebuilder:scaffold:imports
41+
)
42+
43+
var (
44+
scheme = runtime.NewScheme()
45+
setupLog = ctrl.Log.WithName("setup")
46+
)
47+
48+
func init() {
49+
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
50+
51+
utilruntime.Must(backupsv1alpha1.AddToScheme(scheme))
52+
// +kubebuilder:scaffold:scheme
53+
}
54+
55+
func main() {
56+
var metricsAddr string
57+
var enableLeaderElection bool
58+
var probeAddr string
59+
var secureMetrics bool
60+
var enableHTTP2 bool
61+
var tlsOpts []func(*tls.Config)
62+
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
63+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
64+
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
65+
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
66+
"Enable leader election for controller manager. "+
67+
"Enabling this will ensure there is only one active controller manager.")
68+
flag.BoolVar(&secureMetrics, "metrics-secure", true,
69+
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
70+
flag.BoolVar(&enableHTTP2, "enable-http2", false,
71+
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
72+
opts := zap.Options{
73+
Development: false,
74+
}
75+
opts.BindFlags(flag.CommandLine)
76+
flag.Parse()
77+
78+
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
79+
80+
// if the enable-http2 flag is false (the default), http/2 should be disabled
81+
// due to its vulnerabilities. More specifically, disabling http/2 will
82+
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
83+
// Rapid Reset CVEs. For more information see:
84+
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
85+
// - https://github.com/advisories/GHSA-4374-p667-p6c8
86+
disableHTTP2 := func(c *tls.Config) {
87+
setupLog.Info("disabling http/2")
88+
c.NextProtos = []string{"http/1.1"}
89+
}
90+
91+
if !enableHTTP2 {
92+
tlsOpts = append(tlsOpts, disableHTTP2)
93+
}
94+
95+
webhookServer := webhook.NewServer(webhook.Options{
96+
TLSOpts: tlsOpts,
97+
})
98+
99+
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
100+
// More info:
101+
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/server
102+
// - https://book.kubebuilder.io/reference/metrics.html
103+
metricsServerOptions := metricsserver.Options{
104+
BindAddress: metricsAddr,
105+
SecureServing: secureMetrics,
106+
TLSOpts: tlsOpts,
107+
}
108+
109+
if secureMetrics {
110+
// FilterProvider is used to protect the metrics endpoint with authn/authz.
111+
// These configurations ensure that only authorized users and service accounts
112+
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
113+
// https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/filters#WithAuthenticationAndAuthorization
114+
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
115+
116+
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
117+
// generate self-signed certificates for the metrics server. While convenient for development and testing,
118+
// this setup is not recommended for production.
119+
}
120+
121+
// Configure rate limiting for the Kubernetes client
122+
config := ctrl.GetConfigOrDie()
123+
config.QPS = 50.0 // Increased from default 5.0
124+
config.Burst = 100 // Increased from default 10
125+
126+
mgr, err := ctrl.NewManager(config, ctrl.Options{
127+
Scheme: scheme,
128+
Metrics: metricsServerOptions,
129+
WebhookServer: webhookServer,
130+
HealthProbeBindAddress: probeAddr,
131+
LeaderElection: enableLeaderElection,
132+
LeaderElectionID: "core.backups.cozystack.io",
133+
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
134+
// when the Manager ends. This requires the binary to immediately end when the
135+
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
136+
// speeds up voluntary leader transitions as the new leader don't have to wait
137+
// LeaseDuration time first.
138+
//
139+
// In the default scaffold provided, the program ends immediately after
140+
// the manager stops, so would be fine to enable this option. However,
141+
// if you are doing or is intended to do any operation such as perform cleanups
142+
// after the manager stops then its usage might be unsafe.
143+
// LeaderElectionReleaseOnCancel: true,
144+
})
145+
if err != nil {
146+
setupLog.Error(err, "unable to start manager")
147+
os.Exit(1)
148+
}
149+
150+
if err = (&backupcontroller.PlanReconciler{
151+
Client: mgr.GetClient(),
152+
Scheme: mgr.GetScheme(),
153+
}).SetupWithManager(mgr); err != nil {
154+
setupLog.Error(err, "unable to create controller", "controller", "Plan")
155+
os.Exit(1)
156+
}
157+
158+
// +kubebuilder:scaffold:builder
159+
160+
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
161+
setupLog.Error(err, "unable to set up health check")
162+
os.Exit(1)
163+
}
164+
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
165+
setupLog.Error(err, "unable to set up ready check")
166+
os.Exit(1)
167+
}
168+
169+
setupLog.Info("starting manager")
170+
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
171+
setupLog.Error(err, "problem running manager")
172+
os.Exit(1)
173+
}
174+
}

‎internal/backupcontroller/plan_controller.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ func (r *PlanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
6161
}
6262
return ctrl.Result{}, nil
6363
}
64+
65+
// Clear error condition if cron parsing succeeds
66+
if condition := meta.FindStatusCondition(p.Status.Conditions, backupsv1alpha1.PlanConditionError); condition != nil && condition.Status == metav1.ConditionTrue {
67+
meta.SetStatusCondition(&p.Status.Conditions, metav1.Condition{
68+
Type: backupsv1alpha1.PlanConditionError,
69+
Status: metav1.ConditionFalse,
70+
Reason: "Cron spec is valid",
71+
Message: "The cron schedule has been successfully parsed",
72+
})
73+
if err := r.Status().Update(ctx, p); err != nil {
74+
return ctrl.Result{}, err
75+
}
76+
}
77+
6478
tNext := sch.Next(tCheck)
6579

6680
if time.Now().Before(tNext) {

‎packages/core/platform/bundles/paas-full.yaml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ releases:
112112
disableTelemetry: true
113113
{{- end }}
114114

115+
- name: backup-controller
116+
releaseName: backup-controller
117+
chart: cozy-backup-controller
118+
namespace: cozy-backup-controller
119+
dependsOn: [cilium,kubeovn]
120+
115121
- name: lineage-controller-webhook
116122
releaseName: lineage-controller-webhook
117123
chart: cozy-lineage-controller-webhook

‎packages/core/platform/bundles/paas-hosted.yaml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ releases:
5656
disableTelemetry: true
5757
{{- end }}
5858

59+
- name: backup-controller
60+
releaseName: backup-controller
61+
chart: cozy-backup-controller
62+
namespace: cozy-backup-controller
63+
5964
- name: lineage-controller-webhook
6065
releaseName: lineage-controller-webhook
6166
chart: cozy-lineage-controller-webhook
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
apiVersion: v2
2+
name: cozy-backup-controller
3+
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
NAME=backup-controller
2+
NAMESPACE=cozy-backup-controller
3+
4+
include ../../../scripts/common-envs.mk
5+
include ../../../scripts/package.mk
6+
7+
image: image-backup-controller
8+
9+
image-backup-controller:
10+
docker buildx build -f images/backup-controller/Dockerfile ../../.. \
11+
--tag $(REGISTRY)/backup-controller:$(call settag,$(TAG)) \
12+
--cache-from type=registry,ref=$(REGISTRY)/backup-controller:latest \
13+
--cache-to type=inline \
14+
--metadata-file images/backup-controller.json \
15+
$(BUILDX_ARGS)
16+
IMAGE="$(REGISTRY)/backup-controller:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/backup-controller.json -o json -r)" \
17+
yq -i '.backupController.image = strenv(IMAGE)' values.yaml
18+
rm -f images/backup-controller.json
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM golang:1.24-alpine AS builder
2+
3+
ARG TARGETOS
4+
ARG TARGETARCH
5+
6+
WORKDIR /workspace
7+
8+
COPY go.mod go.sum ./
9+
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go mod download
10+
11+
COPY api api/
12+
COPY pkg pkg/
13+
COPY cmd cmd/
14+
COPY internal internal/
15+
16+
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /backup-controller cmd/backup-controller/main.go
17+
18+
FROM scratch
19+
20+
COPY --from=builder /backup-controller /backup-controller
21+
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
22+
23+
ENTRYPOINT ["/backup-controller"]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{{- range $path, $_ := .Files.Glob "definitions/*" }}
2+
---
3+
{{ $.Files.Get $path }}
4+
{{- end }}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: backup-controller
5+
labels:
6+
app: backup-controller
7+
spec:
8+
replicas: {{ .Values.backupController.replicas }}
9+
selector:
10+
matchLabels:
11+
app: backup-controller
12+
template:
13+
metadata:
14+
labels:
15+
app: backup-controller
16+
spec:
17+
tolerations:
18+
- key: "node-role.kubernetes.io/control-plane"
19+
operator: "Exists"
20+
effect: "NoSchedule"
21+
- key: "node-role.kubernetes.io/master"
22+
operator: "Exists"
23+
effect: "NoSchedule"
24+
serviceAccountName: backup-controller
25+
containers:
26+
- name: backup-controller
27+
image: "{{ .Values.backupController.image }}"
28+
args:
29+
- --leader-elect
30+
{{- if .Values.backupController.metrics.enable }}
31+
- --metrics-bind-address={{ .Values.backupController.metrics.bindAddress }}
32+
{{- end }}
33+
{{- if .Values.backupController.debug }}
34+
- --zap-log-level=debug
35+
{{- else }}
36+
- --zap-log-level=info
37+
{{- end }}
38+
ports:
39+
- name: metrics
40+
containerPort: {{ split ":" .Values.backupController.metrics.bindAddress | mustLast }}
41+
- name: health
42+
containerPort: 8081
43+
readinessProbe:
44+
httpGet:
45+
path: /readyz
46+
port: health
47+
initialDelaySeconds: 5
48+
periodSeconds: 10
49+
livenessProbe:
50+
httpGet:
51+
path: /healthz
52+
port: health
53+
initialDelaySeconds: 15
54+
periodSeconds: 20
55+
{{- with .Values.backupController.resources }}
56+
resources: {{- . | toYaml | nindent 10 }}
57+
{{- end }}

0 commit comments

Comments
 (0)