Skip to content

Commit c506004

Browse files
Merge pull request #1904 from stuggi/backup_restore_webhook
[b/r] Add singleton webhook for OpenStackBackupConfig
2 parents f75fe43 + 0922aca commit c506004

9 files changed

Lines changed: 238 additions & 2 deletions

File tree

PROJECT

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,7 @@ resources:
108108
kind: OpenStackBackupConfig
109109
path: github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1
110110
version: v1beta1
111+
webhooks:
112+
validation: true
113+
webhookVersion: v1
111114
version: "3"

api/backup/v1beta1/openstackbackupconfig_types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20+
"context"
21+
2022
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2123
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
2225
)
2326

2427
// BackupLabelingPolicy controls whether backup labeling is active for a resource type
@@ -144,6 +147,18 @@ type OpenStackBackupConfigList struct {
144147
Items []OpenStackBackupConfig `json:"items"`
145148
}
146149

150+
// GetOpenStackBackupConfigs returns the OpenStackBackupConfig resources in the given namespace.
151+
func GetOpenStackBackupConfigs(ctx context.Context, namespace string, c client.Client) (*OpenStackBackupConfigList, error) {
152+
configList := &OpenStackBackupConfigList{}
153+
listOpts := []client.ListOption{
154+
client.InNamespace(namespace),
155+
}
156+
if err := c.List(ctx, configList, listOpts...); err != nil {
157+
return nil, err
158+
}
159+
return configList, nil
160+
}
161+
147162
func init() {
148163
SchemeBuilder.Register(&OpenStackBackupConfig{}, &OpenStackBackupConfigList{})
149164
}

bindata/operator/operator.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,26 @@ metadata:
382382
cert-manager.io/inject-ca-from: '{{ .OperatorNamespace }}/openstack-operator-serving-cert'
383383
name: openstack-operator-validating-webhook-configuration
384384
webhooks:
385+
- admissionReviewVersions:
386+
- v1
387+
clientConfig:
388+
service:
389+
name: openstack-operator-webhook-service
390+
namespace: '{{ .OperatorNamespace }}'
391+
path: /validate-backup-openstack-org-v1beta1-openstackbackupconfig
392+
failurePolicy: Fail
393+
name: vopenstackbackupconfig-v1beta1.kb.io
394+
rules:
395+
- apiGroups:
396+
- backup.openstack.org
397+
apiVersions:
398+
- v1beta1
399+
operations:
400+
- CREATE
401+
- UPDATE
402+
resources:
403+
- openstackbackupconfigs
404+
sideEffects: None
385405
- admissionReviewVersions:
386406
- v1
387407
clientConfig:

cmd/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import (
5050
backupv1beta1 "github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1"
5151
backupcontroller "github.com/openstack-k8s-operators/openstack-operator/internal/controller/backup"
5252

53+
webhookbackupv1beta1 "github.com/openstack-k8s-operators/openstack-operator/internal/webhook/backup/v1beta1"
5354
// +kubebuilder:scaffold:imports
5455
certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
5556
k8s_networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1"
@@ -425,6 +426,11 @@ func main() {
425426
setupLog.Error(err, "unable to create webhook", "webhook", "OpenStackDataPlaneService")
426427
os.Exit(1)
427428
}
429+
// nolint:goconst
430+
if err := webhookbackupv1beta1.SetupOpenStackBackupConfigWebhookWithManager(mgr); err != nil {
431+
setupLog.Error(err, "unable to create webhook", "webhook", "OpenStackBackupConfig")
432+
os.Exit(1)
433+
}
428434
checker = mgr.GetWebhookServer().StartedChecker()
429435
}
430436
// +kubebuilder:scaffold:builder

config/webhook/manifests.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,26 @@ kind: ValidatingWebhookConfiguration
190190
metadata:
191191
name: validating-webhook-configuration
192192
webhooks:
193+
- admissionReviewVersions:
194+
- v1
195+
clientConfig:
196+
service:
197+
name: webhook-service
198+
namespace: system
199+
path: /validate-backup-openstack-org-v1beta1-openstackbackupconfig
200+
failurePolicy: Fail
201+
name: vopenstackbackupconfig-v1beta1.kb.io
202+
rules:
203+
- apiGroups:
204+
- backup.openstack.org
205+
apiVersions:
206+
- v1beta1
207+
operations:
208+
- CREATE
209+
- UPDATE
210+
resources:
211+
- openstackbackupconfigs
212+
sideEffects: None
193213
- admissionReviewVersions:
194214
- v1
195215
clientConfig:

internal/openstack/backup.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,26 @@ import (
3535
// Automatically creates an OpenStackBackupConfig CR when OpenStackControlPlane is created
3636
// Similar pattern to ReconcileVersion
3737
func ReconcileBackupConfig(ctx context.Context, instance *corev1beta1.OpenStackControlPlane, helper *helper.Helper) (ctrl.Result, *backupv1beta1.OpenStackBackupConfig, error) {
38+
Log := GetLogger(ctx)
39+
40+
// Check if a BackupConfig already exists (may have been pre-created by the user)
41+
configList, err := backupv1beta1.GetOpenStackBackupConfigs(ctx, instance.Namespace, helper.GetClient())
42+
if err != nil {
43+
return ctrl.Result{}, nil, fmt.Errorf("failed to list OpenStackBackupConfigs: %w", err)
44+
}
45+
if len(configList.Items) > 0 {
46+
existing := &configList.Items[0]
47+
Log.Info("Using existing OpenStackBackupConfig", "name", existing.Name)
48+
return ctrl.Result{}, existing, nil
49+
}
50+
3851
backupConfig := &backupv1beta1.OpenStackBackupConfig{
3952
ObjectMeta: metav1.ObjectMeta{
4053
Name: instance.Name,
4154
Namespace: instance.Namespace,
4255
},
4356
}
4457

45-
Log := GetLogger(ctx)
46-
4758
defaultLabeling := backupv1beta1.BackupLabelingEnabled
4859

4960
op, err := controllerutil.CreateOrPatch(ctx, helper.GetClient(), backupConfig, func() error {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
Copyright 2026.
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 v1beta1 contains webhooks for backup API resources.
18+
package v1beta1
19+
20+
import (
21+
"context"
22+
"fmt"
23+
24+
apierrors "k8s.io/apimachinery/pkg/api/errors"
25+
"k8s.io/apimachinery/pkg/runtime"
26+
"k8s.io/apimachinery/pkg/runtime/schema"
27+
"k8s.io/apimachinery/pkg/util/validation/field"
28+
ctrl "sigs.k8s.io/controller-runtime"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
logf "sigs.k8s.io/controller-runtime/pkg/log"
31+
"sigs.k8s.io/controller-runtime/pkg/webhook"
32+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
33+
34+
backupv1beta1 "github.com/openstack-k8s-operators/openstack-operator/api/backup/v1beta1"
35+
)
36+
37+
var openstackbackupconfiglog = logf.Log.WithName("openstackbackupconfig-resource")
38+
39+
var backupConfigWebhookClient client.Client
40+
41+
// SetupOpenStackBackupConfigWebhookWithManager registers the webhook for OpenStackBackupConfig in the manager.
42+
func SetupOpenStackBackupConfigWebhookWithManager(mgr ctrl.Manager) error {
43+
if backupConfigWebhookClient == nil {
44+
backupConfigWebhookClient = mgr.GetClient()
45+
}
46+
47+
return ctrl.NewWebhookManagedBy(mgr).For(&backupv1beta1.OpenStackBackupConfig{}).
48+
WithValidator(&OpenStackBackupConfigCustomValidator{}).
49+
Complete()
50+
}
51+
52+
// +kubebuilder:webhook:path=/validate-backup-openstack-org-v1beta1-openstackbackupconfig,mutating=false,failurePolicy=fail,sideEffects=None,groups=backup.openstack.org,resources=openstackbackupconfigs,verbs=create;update,versions=v1beta1,name=vopenstackbackupconfig-v1beta1.kb.io,admissionReviewVersions=v1
53+
54+
// OpenStackBackupConfigCustomValidator struct is responsible for validating the OpenStackBackupConfig resource
55+
// when it is created, updated, or deleted.
56+
//
57+
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
58+
// as this struct is used only for temporary operations and does not need to be deeply copied.
59+
type OpenStackBackupConfigCustomValidator struct{}
60+
61+
var _ webhook.CustomValidator = &OpenStackBackupConfigCustomValidator{}
62+
63+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type OpenStackBackupConfig.
64+
func (v *OpenStackBackupConfigCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
65+
backupConfig, ok := obj.(*backupv1beta1.OpenStackBackupConfig)
66+
if !ok {
67+
return nil, fmt.Errorf("expected an OpenStackBackupConfig object but got %T", obj)
68+
}
69+
openstackbackupconfiglog.Info("Validation for OpenStackBackupConfig upon creation", "name", backupConfig.GetName())
70+
71+
configList, err := backupv1beta1.GetOpenStackBackupConfigs(ctx, backupConfig.Namespace, backupConfigWebhookClient)
72+
if err != nil {
73+
return nil, apierrors.NewForbidden(
74+
schema.GroupResource{
75+
Group: backupv1beta1.GroupVersion.WithKind("OpenStackBackupConfig").Group,
76+
Resource: backupv1beta1.GroupVersion.WithKind("OpenStackBackupConfig").Kind,
77+
}, backupConfig.GetName(), &field.Error{
78+
Type: field.ErrorTypeForbidden,
79+
Detail: err.Error(),
80+
},
81+
)
82+
}
83+
84+
if len(configList.Items) >= 1 {
85+
return nil, apierrors.NewForbidden(
86+
schema.GroupResource{
87+
Group: backupv1beta1.GroupVersion.WithKind("OpenStackBackupConfig").Group,
88+
Resource: backupv1beta1.GroupVersion.WithKind("OpenStackBackupConfig").Kind,
89+
}, backupConfig.GetName(), &field.Error{
90+
Type: field.ErrorTypeForbidden,
91+
Detail: "Only one OpenStackBackupConfig instance is supported per namespace.",
92+
},
93+
)
94+
}
95+
96+
return nil, nil
97+
}
98+
99+
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type OpenStackBackupConfig.
100+
func (v *OpenStackBackupConfigCustomValidator) ValidateUpdate(_ context.Context, _, newObj runtime.Object) (admission.Warnings, error) {
101+
backupConfig, ok := newObj.(*backupv1beta1.OpenStackBackupConfig)
102+
if !ok {
103+
return nil, fmt.Errorf("expected an OpenStackBackupConfig object for the newObj but got %T", newObj)
104+
}
105+
openstackbackupconfiglog.Info("Validation for OpenStackBackupConfig upon update", "name", backupConfig.GetName())
106+
107+
return nil, nil
108+
}
109+
110+
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type OpenStackBackupConfig.
111+
func (v *OpenStackBackupConfigCustomValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
112+
backupConfig, ok := obj.(*backupv1beta1.OpenStackBackupConfig)
113+
if !ok {
114+
return nil, fmt.Errorf("expected an OpenStackBackupConfig object but got %T", obj)
115+
}
116+
openstackbackupconfiglog.Info("Validation for OpenStackBackupConfig upon deletion", "name", backupConfig.GetName())
117+
118+
return nil, nil
119+
}

test/functional/ctlplane/openstackbackupconfig_controller_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
. "github.com/onsi/gomega" //revive:disable:dot-imports
2222

2323
k8s_corev1 "k8s.io/api/core/v1"
24+
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2526
"k8s.io/apimachinery/pkg/types"
2627

@@ -879,4 +880,42 @@ var _ = Describe("OpenStackBackupConfig controller", func() {
879880
}, timeout, interval).Should(Succeed())
880881
})
881882
})
883+
884+
When("A second OpenStackBackupConfig is created in the same namespace", func() {
885+
BeforeEach(func() {
886+
backupConfigName = types.NamespacedName{
887+
Name: "first-backup-config",
888+
Namespace: namespace,
889+
}
890+
891+
backupConfig := CreateBackupConfig(backupConfigName)
892+
DeferCleanup(th.DeleteInstance, backupConfig)
893+
})
894+
895+
It("Should be rejected by the webhook", func() {
896+
secondConfig := &backupv1.OpenStackBackupConfig{
897+
ObjectMeta: metav1.ObjectMeta{
898+
Name: "second-backup-config",
899+
Namespace: namespace,
900+
},
901+
Spec: backupv1.OpenStackBackupConfigSpec{
902+
DefaultRestoreOrder: "10",
903+
Secrets: backupv1.ResourceBackupConfig{
904+
Labeling: backupLabelingPtr(backupv1.BackupLabelingEnabled),
905+
},
906+
ConfigMaps: backupv1.ResourceBackupConfig{
907+
Labeling: backupLabelingPtr(backupv1.BackupLabelingEnabled),
908+
ExcludeNames: []string{"kube-root-ca.crt", "openshift-service-ca.crt"},
909+
},
910+
NetworkAttachmentDefinitions: backupv1.ResourceBackupConfig{
911+
Labeling: backupLabelingPtr(backupv1.BackupLabelingEnabled),
912+
},
913+
},
914+
}
915+
err := k8sClient.Create(ctx, secondConfig)
916+
Expect(err).To(HaveOccurred())
917+
Expect(k8s_errors.IsForbidden(err)).To(BeTrue())
918+
Expect(err.Error()).To(ContainSubstring("Only one OpenStackBackupConfig instance is supported per namespace"))
919+
})
920+
})
882921
})

test/functional/ctlplane/suite_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import (
7676
mariadb_test "github.com/openstack-k8s-operators/mariadb-operator/api/test/helpers"
7777
ovn_test "github.com/openstack-k8s-operators/ovn-operator/api/test/helpers"
7878

79+
backupwebhook "github.com/openstack-k8s-operators/openstack-operator/internal/webhook/backup/v1beta1"
7980
clientwebhook "github.com/openstack-k8s-operators/openstack-operator/internal/webhook/client/v1beta1"
8081
corewebhook "github.com/openstack-k8s-operators/openstack-operator/internal/webhook/core/v1beta1"
8182
dataplanewebhook "github.com/openstack-k8s-operators/openstack-operator/internal/webhook/dataplane/v1beta1"
@@ -378,6 +379,8 @@ var _ = BeforeSuite(func() {
378379
Expect(err).NotTo(HaveOccurred())
379380
err = dataplanewebhook.SetupOpenStackDataPlaneNodeSetWebhookWithManager(k8sManager)
380381
Expect(err).NotTo(HaveOccurred())
382+
err = backupwebhook.SetupOpenStackBackupConfigWebhookWithManager(k8sManager)
383+
Expect(err).NotTo(HaveOccurred())
381384

382385
core_ctrl.SetupVersionDefaults()
383386
openstack.SetupServiceOperatorDefaults()

0 commit comments

Comments
 (0)