Skip to content

Commit 35320f4

Browse files
committed
ensure vap resources in a native way
1 parent cfd85f7 commit 35320f4

File tree

3 files changed

+63
-20
lines changed

3 files changed

+63
-20
lines changed

pkg/operator/controllers/guardrails/guardrails_vap.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ func (r *Reconciler) deployVAP(ctx context.Context, instance *arov1alpha1.Cluste
118118
return nil
119119
}
120120

121-
// ensureVAPPolicy creates the ValidatingAdmissionPolicy if it does not exist.
121+
// ensureVAPPolicy creates or updates the ValidatingAdmissionPolicy.
122+
// The dynamic helper's Ensure method detects native Kubernetes resources
123+
// and uses server-side apply, so all policy fields are correctly reconciled.
122124
func (r *Reconciler) ensureVAPPolicy(ctx context.Context, filename string) error {
123125
data, err := fs.ReadFile(vapPolicies, filepath.Join(vapPolicyPath, filename))
124126
if err != nil {
@@ -131,8 +133,10 @@ func (r *Reconciler) ensureVAPPolicy(ctx context.Context, filename string) error
131133
return r.dh.Ensure(ctx, uns)
132134
}
133135

134-
// ensureVAPBinding deletes an existing binding and recreates it so the
135-
// validationActions field always reflects the current enforcement setting.
136+
// ensureVAPBinding creates or updates the ValidatingAdmissionPolicyBinding.
137+
// The dynamic helper's Ensure method detects native Kubernetes resources
138+
// and uses server-side apply, so validationActions changes are applied
139+
// atomically without needing a delete-then-create workaround.
136140
func (r *Reconciler) ensureVAPBinding(ctx context.Context, policyName, validationAction string) error {
137141
bindingFile := policyName + "-binding.yaml"
138142
tmpl, err := template.ParseFS(vapBindings, filepath.Join(vapBindingPath, bindingFile))
@@ -155,17 +159,6 @@ func (r *Reconciler) ensureVAPBinding(ctx context.Context, policyName, validatio
155159
return err
156160
}
157161

158-
bindingName := uns.GetName()
159-
gk := uns.GroupVersionKind().GroupKind().String()
160-
ver := uns.GroupVersionKind().Version
161-
162-
// Delete then recreate to pick up enforcement changes; ensureUnstructuredObj
163-
// in the dynamic helper only checks Gatekeeper enforcementAction, not VAP
164-
// validationActions, so an in-place update would silently no-op.
165-
if err := r.dh.EnsureDeletedGVR(ctx, gk, "", bindingName, ver); err != nil {
166-
r.log.Warnf("failed to delete existing VAP binding %s for recreation: %s", bindingName, err.Error())
167-
}
168-
169162
return r.dh.Ensure(ctx, uns)
170163
}
171164

pkg/util/dynamichelper/dynamichelper.go

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,24 @@ package dynamichelper
55

66
import (
77
"context"
8+
"encoding/json"
9+
"fmt"
810
"strings"
911

1012
"github.com/sirupsen/logrus"
1113

1214
kerrors "k8s.io/apimachinery/pkg/api/errors"
1315
"k8s.io/apimachinery/pkg/api/meta"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1417
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1518
kruntime "k8s.io/apimachinery/pkg/runtime"
1619
"k8s.io/apimachinery/pkg/runtime/schema"
20+
"k8s.io/apimachinery/pkg/types"
1721
"k8s.io/client-go/dynamic"
1822
"k8s.io/client-go/kubernetes/scheme"
1923
"k8s.io/client-go/rest"
2024
"k8s.io/client-go/util/retry"
25+
"k8s.io/utils/pointer"
2126

2227
"sigs.k8s.io/controller-runtime/pkg/client"
2328

@@ -97,9 +102,19 @@ func (dh *dynamicHelper) EnsureDeletedGVR(ctx context.Context, groupKind, namesp
97102
func (dh *dynamicHelper) Ensure(ctx context.Context, objs ...kruntime.Object) error {
98103
for _, o := range objs {
99104
if un, ok := o.(*unstructured.Unstructured); ok {
100-
err := dh.ensureUnstructuredObj(ctx, un)
101-
if err != nil {
102-
return err
105+
// ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
106+
// are handled via server-side apply so that all fields are
107+
// correctly reconciled. The Gatekeeper-specific path only
108+
// compares enforcementAction and would silently skip updates
109+
// to other resource types.
110+
if isAdmissionRegistrationResource(un) {
111+
if err := dh.ensureByServerSideApply(ctx, un); err != nil {
112+
return err
113+
}
114+
} else {
115+
if err := dh.ensureGatekeeperConstraint(ctx, un); err != nil {
116+
return err
117+
}
103118
}
104119
continue
105120
}
@@ -160,6 +175,40 @@ func (dh *dynamicHelper) mergeWithLogic(name, groupKind string, old, new kruntim
160175
return clienthelper.Merge(old.(client.Object), new.(client.Object))
161176
}
162177

178+
// isAdmissionRegistrationResource returns true for ValidatingAdmissionPolicy
179+
// and ValidatingAdmissionPolicyBinding resources that should be managed via
180+
// server-side apply rather than the Gatekeeper-specific path.
181+
func isAdmissionRegistrationResource(uns *unstructured.Unstructured) bool {
182+
return uns.GroupVersionKind().Group == "admissionregistration.k8s.io"
183+
}
184+
185+
// ensureByServerSideApply creates or updates a single unstructured object
186+
// using server-side apply. This correctly reconciles all fields, unlike
187+
// ensureUnstructuredObj which only compares Gatekeeper's enforcementAction.
188+
func (dh *dynamicHelper) ensureByServerSideApply(ctx context.Context, uns *unstructured.Unstructured) error {
189+
gvr, err := dh.Resolve(uns.GroupVersionKind().GroupKind().String(), uns.GroupVersionKind().Version)
190+
if err != nil {
191+
return err
192+
}
193+
194+
data, err := json.Marshal(uns)
195+
if err != nil {
196+
return fmt.Errorf("marshalling %s/%s: %w", uns.GroupVersionKind().GroupKind(), uns.GetName(), err)
197+
}
198+
199+
dh.log.Infof("Apply %s", keyFunc(uns.GroupVersionKind().GroupKind(), uns.GetNamespace(), uns.GetName()))
200+
_, err = dh.dynamicClient.Resource(*gvr).
201+
Namespace(uns.GetNamespace()).
202+
Patch(ctx, uns.GetName(), types.ApplyPatchType, data, metav1.PatchOptions{
203+
FieldManager: "aro-operator",
204+
Force: pointer.Bool(true),
205+
})
206+
if err != nil {
207+
return fmt.Errorf("server-side apply %s/%s: %w", uns.GroupVersionKind().GroupKind(), uns.GetName(), err)
208+
}
209+
return nil
210+
}
211+
163212
func makeURLSegments(gvr *schema.GroupVersionResource, namespace, name string) (url []string) {
164213
if gvr.Group == "" {
165214
url = append(url, "api")

pkg/util/dynamichelper/guardrails.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ import (
2323
_ "github.com/Azure/ARO-RP/pkg/util/scheme"
2424
)
2525

26-
// the UnstructuredObj related stuff is specifically for the Guardrails
27-
// to handle the gatekeeper Constraint as it does not have a scheme that can be imported
28-
func (dh *dynamicHelper) ensureUnstructuredObj(ctx context.Context, uns *unstructured.Unstructured) error {
26+
// ensureGatekeeperConstraint handles Gatekeeper Constraint resources which
27+
// do not have a scheme that can be imported. It only compares the
28+
// spec.enforcementAction field to decide whether an update is needed.
29+
func (dh *dynamicHelper) ensureGatekeeperConstraint(ctx context.Context, uns *unstructured.Unstructured) error {
2930
gvr, err := dh.Resolve(uns.GroupVersionKind().GroupKind().String(), uns.GroupVersionKind().Version)
3031
if err != nil {
3132
return err

0 commit comments

Comments
 (0)