Skip to content

Commit 2c59cbe

Browse files
Capture warnings using custom client
- Updates the VAP to emit a warning if the SynchronizedCondition is Unknown or unset. - Updates the test suite to capture warnings using a custom client.
1 parent 023e47e commit 2c59cbe

File tree

3 files changed

+148
-14
lines changed

3 files changed

+148
-14
lines changed

manifests/0000_30_cluster-api_09_admission-policies.yaml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,12 +283,27 @@ data:
283283
apiVersions: ["*"]
284284
operations: ["UPDATE"]
285285
resources: ["machines"]
286+
variables:
287+
- name: syncCond
288+
expression: >
289+
has(object.status.conditions) ?
290+
object.status.conditions.exists(c, c.type == "Synchronized") :
291+
false
292+
- name: syncBad
293+
expression: >
294+
has(object.status.conditions) ?
295+
object.status.conditions.exists(c,
296+
c.type == "Synchronized" &&
297+
(c.status == "False" || c.status == "Unknown")
298+
) : true
299+
- name: authAPIChanged
300+
expression: >
301+
oldObject.spec.authoritativeAPI != object.spec.authoritativeAPI
286302
287303
# All validations must evaluate to true
288304
validations:
289-
- expression: '!(has(object.status) && has(object.status.conditions) && object.status.conditions.exists(c, c.type == "Synchronized" && c.status == "False") && (oldObject.spec.authoritativeAPI != object.spec.authoritativeAPI))'
305+
- expression: '!(( !variables.syncCond || variables.syncBad ) && variables.authAPIChanged)'
290306
message: 'Warning: updating .spec.authoritativeAPI when Synchronized=false on resource means changes may not take effect'
291-
validationActions: [Warn]
292307
---
293308
apiVersion: admissionregistration.k8s.io/v1
294309
kind: ValidatingAdmissionPolicyBinding

pkg/admissionpolicy/testutils/util.go

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

1919
import (
20+
"context"
2021
"errors"
22+
"fmt"
2123
"io"
2224
"os"
2325
"path/filepath"
2426
"strings"
27+
"sync"
2528

2629
corev1 "k8s.io/api/core/v1"
2730

@@ -32,6 +35,7 @@ import (
3235
"k8s.io/apimachinery/pkg/runtime"
3336
"k8s.io/apimachinery/pkg/util/yaml"
3437
"k8s.io/client-go/kubernetes/scheme"
38+
"k8s.io/client-go/rest"
3539

3640
"sigs.k8s.io/controller-runtime/pkg/client"
3741
)
@@ -261,3 +265,54 @@ func LoadTransportConfigMaps() map[string][]client.Object {
261265

262266
return mapObjs
263267
}
268+
269+
// WarningCollector is to provide a way to collect
270+
// kube client warnings, intended for testing VAPs that return warnings.
271+
type WarningCollector struct {
272+
sync.Mutex
273+
messages []string
274+
}
275+
276+
// HandleWarningHeaderWithContext implements rest.WarningHandlerWithContext.
277+
// For test simplicity, only the message is captured; code and agent are ignored.
278+
func (w *WarningCollector) HandleWarningHeaderWithContext(_ context.Context, code int, _ string, message string) {
279+
w.Lock()
280+
w.messages = append(w.messages, message)
281+
w.Unlock()
282+
}
283+
284+
// Messages returns messages collected by a warning collector.
285+
func (w *WarningCollector) Messages() []string {
286+
w.Lock()
287+
defer w.Unlock()
288+
289+
// return a copy for thread-safety
290+
out := make([]string, len(w.messages))
291+
copy(out, w.messages)
292+
293+
return out
294+
}
295+
296+
// Reset clears the messages, used between tests to reset state.
297+
func (w *WarningCollector) Reset() {
298+
w.Lock()
299+
w.messages = nil
300+
w.Unlock()
301+
}
302+
303+
// SetupClientWithWarningCollector creates a new client.Client, with a warning handler that writes to a returned WarningCollector.
304+
func SetupClientWithWarningCollector(cfg *rest.Config, scheme *runtime.Scheme) (client.Client, *WarningCollector, error) {
305+
warnSink := &WarningCollector{}
306+
// copy to avoid mutating the passed-in config
307+
newcfg := rest.CopyConfig(cfg)
308+
309+
newcfg.WarningHandlerWithContext = warnSink
310+
311+
// Build the client with this config
312+
client, err := client.New(newcfg, client.Options{Scheme: scheme})
313+
if err != nil {
314+
return nil, nil, fmt.Errorf("error creating new client: %w", err)
315+
}
316+
317+
return client, warnSink, nil
318+
}

pkg/controllers/machinesync/machine_sync_controller_test.go

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,7 +1364,11 @@ var _ = Describe("With a running MachineSync Reconciler", func() {
13641364

13651365
})
13661366

1367-
FContext("Updates to MAPI machine warns user if the Synchronized condition is set to false", func() {
1367+
Context("Updates to MAPI machine warns user if the Synchronized condition is set to false", func() {
1368+
var warnClient client.Client
1369+
var warnSink *admissiontestutils.WarningCollector
1370+
var warnKomega komega.Komega
1371+
13681372
BeforeEach(func() {
13691373
By("Waiting for VAP to be ready")
13701374
machineVap = &admissionregistrationv1.ValidatingAdmissionPolicy{}
@@ -1406,17 +1410,26 @@ var _ = Describe("With a running MachineSync Reconciler", func() {
14061410
sentinelMachine := mapiMachineBuilder.WithName("sentinel-machine").WithAuthoritativeAPI(mapiv1beta1.MachineAuthorityClusterAPI).Build()
14071411
Eventually(k8sClient.Create(ctx, sentinelMachine), timeout).Should(Succeed())
14081412

1409-
capiSentinelMachine := clusterv1resourcebuilder.Machine().WithName("sentinel-machine").WithNamespace(capiNamespace.Name).Build()
1410-
Expect(k8sClient.Create(ctx, capiSentinelMachine)).To(Succeed())
1413+
var err error
1414+
warnClient, warnSink, err = admissiontestutils.SetupClientWithWarningCollector(cfg, testScheme)
1415+
Expect(err).To(Not(HaveOccurred()))
14111416

1412-
Eventually(k.Get(capiSentinelMachine)).Should(Succeed())
1417+
warnKomega = komega.New(warnClient)
1418+
1419+
Eventually(func(g Gomega) {
1420+
warnSink.Reset() // keep each probe self-contained
1421+
1422+
err := warnKomega.Update(sentinelMachine, func() {
1423+
sentinelMachine.ObjectMeta.Labels = map[string]string{"test-sentinel": "fubar"}
1424+
})()
1425+
g.Expect(err).NotTo(HaveOccurred())
1426+
1427+
g.Expect(warnSink.Messages()).To(ContainElement(ContainSubstring("policy in place")))
1428+
}, timeout).Should(Succeed())
14131429

1414-
Eventually(k.Update(sentinelMachine, func() {
1415-
sentinelMachine.ObjectMeta.Labels = map[string]string{"test-sentinel": "fubar"}
1416-
}), timeout).Should(Succeed())
14171430
})
14181431

1419-
It("warns the user when the machine is still synchronzing", func() {
1432+
It("warns the user when the machine is still synchronizing", func() {
14201433
By("Setting the Synchronized condition to False")
14211434
Eventually(k.UpdateStatus(mapiMachine, func() {
14221435
mapiMachine.Status.Conditions = []mapiv1beta1.Condition{
@@ -1430,12 +1443,63 @@ var _ = Describe("With a running MachineSync Reconciler", func() {
14301443
}
14311444
})).Should(Succeed())
14321445

1433-
By("Attempting to update the authoritativeAPI should be blocked")
1434-
Eventually(k.Update(mapiMachine, func() {
1435-
mapiMachine.Spec.AuthoritativeAPI = mapiv1beta1.MachineAuthorityClusterAPI
1436-
}), timeout).Should(Succeed()) // This should succeed but show a warning?
1446+
By("Attempting to update the authoritativeAPI should emit a warning")
1447+
Eventually(func(g Gomega) {
1448+
warnSink.Reset() // keep each probe self-contained
1449+
1450+
err := warnKomega.Update(mapiMachine, func() {
1451+
mapiMachine.Spec.AuthoritativeAPI = mapiv1beta1.MachineAuthorityClusterAPI
1452+
})()
1453+
g.Expect(err).NotTo(HaveOccurred())
1454+
1455+
g.Expect(warnSink.Messages()).To(ContainElement(ContainSubstring("updating .spec.authoritativeAPI when Synchronized=false on resource means changes may not take effect")))
1456+
}, timeout).Should(Succeed())
14371457
})
1458+
It("warns the user when the machine synchronisation is unknown", func() {
1459+
By("Setting the Synchronized condition to Unknown")
1460+
Eventually(k.UpdateStatus(mapiMachine, func() {
1461+
mapiMachine.Status.Conditions = []mapiv1beta1.Condition{
1462+
{
1463+
Type: consts.SynchronizedCondition,
1464+
Status: corev1.ConditionUnknown,
1465+
Reason: "ErrorReason",
1466+
Message: "Error message",
1467+
LastTransitionTime: metav1.Now(),
1468+
},
1469+
}
1470+
})).Should(Succeed())
1471+
1472+
By("Attempting to update the authoritativeAPI should emit a warning")
1473+
Eventually(func(g Gomega) {
1474+
warnSink.Reset() // keep each probe self-contained
14381475

1476+
err := warnKomega.Update(mapiMachine, func() {
1477+
mapiMachine.Spec.AuthoritativeAPI = mapiv1beta1.MachineAuthorityClusterAPI
1478+
})()
1479+
g.Expect(err).NotTo(HaveOccurred())
1480+
1481+
g.Expect(warnSink.Messages()).To(ContainElement(ContainSubstring("updating .spec.authoritativeAPI when Synchronized=false on resource means changes may not take effect")))
1482+
}, timeout).Should(Succeed())
1483+
})
1484+
1485+
It("warns the user when the machine has no synchronized condition", func() {
1486+
By("Setting the conditions to empty")
1487+
Eventually(k.UpdateStatus(mapiMachine, func() {
1488+
mapiMachine.Status.Conditions = []mapiv1beta1.Condition{}
1489+
})).Should(Succeed())
1490+
1491+
By("Attempting to update the authoritativeAPI should emit a warning")
1492+
Eventually(func(g Gomega) {
1493+
warnSink.Reset() // keep each probe self-contained
1494+
1495+
err := warnKomega.Update(mapiMachine, func() {
1496+
mapiMachine.Spec.AuthoritativeAPI = mapiv1beta1.MachineAuthorityClusterAPI
1497+
})()
1498+
g.Expect(err).NotTo(HaveOccurred())
1499+
1500+
g.Expect(warnSink.Messages()).To(ContainElement(ContainSubstring("updating .spec.authoritativeAPI when Synchronized=false on resource means changes may not take effect")))
1501+
}, timeout).Should(Succeed())
1502+
})
14391503
})
14401504

14411505
})

0 commit comments

Comments
 (0)