Skip to content

Commit 0a857c4

Browse files
Merge pull request #398 from theobarberbany/tb/sync-warn
OCPCLOUD-2636: Warn users when attempting to change Authoritative API but Synchronized condition is false
2 parents a1a97d6 + b065cc9 commit 0a857c4

File tree

3 files changed

+242
-2
lines changed

3 files changed

+242
-2
lines changed

manifests/0000_30_cluster-api_09_admission-policies.yaml

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,14 +243,13 @@ data:
243243
name: openshift-prevent-migration-when-machine-updating
244244
spec:
245245
failurePolicy: Fail
246-
247246
matchConstraints:
248247
resourceRules:
249248
- apiGroups: ["machine.openshift.io"]
250249
apiVersions: ["*"]
251250
operations: ["UPDATE"]
252251
resources: ["machines"]
253-
252+
254253
# All validations must evaluate to true
255254
validations:
256255
- expression: '!(has(object.status) && has(object.status.phase) && object.status.phase == "Provisioning" && (oldObject.spec.authoritativeAPI != object.spec.authoritativeAPI))'
@@ -270,6 +269,54 @@ data:
270269
policyName: openshift-prevent-migration-when-machine-updating
271270
validationActions:
272271
- Deny
272+
---
273+
apiVersion: admissionregistration.k8s.io/v1
274+
kind: ValidatingAdmissionPolicy
275+
metadata:
276+
name: openshift-provide-warning-when-not-synchronized
277+
spec:
278+
failurePolicy: Ignore
279+
280+
matchConstraints:
281+
resourceRules:
282+
- apiGroups: ["machine.openshift.io"]
283+
apiVersions: ["*"]
284+
operations: ["UPDATE"]
285+
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+
variables.syncCond &&
295+
object.status.conditions.exists(c,
296+
c.type == "Synchronized" &&
297+
(c.status == "False" || c.status == "Unknown")
298+
)
299+
- name: authAPIChanged
300+
expression: >
301+
oldObject.spec.authoritativeAPI != object.spec.authoritativeAPI
302+
303+
# All validations must evaluate to true
304+
validations:
305+
- expression: '!variables.authAPIChanged || (variables.syncCond && !variables.syncBad)'
306+
message: 'Updating .spec.authoritativeAPI when the Synchronized condition is not true means changes may not take effect'
307+
---
308+
apiVersion: admissionregistration.k8s.io/v1
309+
kind: ValidatingAdmissionPolicyBinding
310+
metadata:
311+
name: openshift-provide-warning-when-not-synchronized
312+
spec:
313+
matchResources:
314+
namespaceSelector:
315+
matchLabels:
316+
kubernetes.io/metadata.name: openshift-machine-api
317+
policyName: openshift-provide-warning-when-not-synchronized
318+
validationActions:
319+
- Warn
273320
---
274321
apiVersion: v1
275322
kind: ConfigMap

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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,144 @@ var _ = Describe("With a running MachineSync Reconciler", func() {
13641364

13651365
})
13661366

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+
1372+
BeforeEach(func() {
1373+
By("Waiting for VAP to be ready")
1374+
machineVap = &admissionregistrationv1.ValidatingAdmissionPolicy{}
1375+
Eventually(k8sClient.Get(ctx, client.ObjectKey{Name: "openshift-provide-warning-when-not-synchronized"}, machineVap), timeout).Should(Succeed())
1376+
Eventually(k.Update(machineVap, func() {
1377+
machineVap.Spec.Validations = append(machineVap.Spec.Validations, admissionregistrationv1.Validation{
1378+
Expression: "!(has(object.metadata.labels) && \"test-sentinel\" in object.metadata.labels)",
1379+
Message: "policy in place",
1380+
})
1381+
})).Should(Succeed())
1382+
1383+
Eventually(k.Object(machineVap), timeout).Should(
1384+
HaveField("Status.ObservedGeneration", BeNumerically(">=", 2)),
1385+
)
1386+
1387+
By("Updating the VAP binding")
1388+
policyBinding = &admissionregistrationv1.ValidatingAdmissionPolicyBinding{}
1389+
Eventually(k8sClient.Get(ctx, client.ObjectKey{
1390+
Name: "openshift-provide-warning-when-not-synchronized"}, policyBinding), timeout).Should(Succeed())
1391+
1392+
Eventually(k.Update(policyBinding, func() {
1393+
// We need to update the namespace in our namespaceSelector,
1394+
// since also use `GenerateName` here
1395+
policyBinding.Spec.MatchResources.NamespaceSelector.MatchLabels = map[string]string{
1396+
"kubernetes.io/metadata.name": mapiNamespace.GetName(),
1397+
}
1398+
}), timeout).Should(Succeed())
1399+
1400+
// Wait until the binding shows the patched values
1401+
Eventually(k.Object(policyBinding), timeout).Should(
1402+
SatisfyAll(
1403+
HaveField("Spec.MatchResources.NamespaceSelector.MatchLabels",
1404+
HaveKeyWithValue("kubernetes.io/metadata.name",
1405+
mapiNamespace.GetName())),
1406+
),
1407+
)
1408+
1409+
By("Creating a throwaway MAPI machine")
1410+
sentinelMachine := mapiMachineBuilder.WithName("sentinel-machine").WithAuthoritativeAPI(mapiv1beta1.MachineAuthorityClusterAPI).Build()
1411+
Eventually(k8sClient.Create(ctx, sentinelMachine), timeout).Should(Succeed())
1412+
1413+
var err error
1414+
warnClient, warnSink, err = admissiontestutils.SetupClientWithWarningCollector(cfg, testScheme)
1415+
Expect(err).To(Not(HaveOccurred()))
1416+
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())
1429+
1430+
})
1431+
1432+
It("warns the user when the machine is still synchronizing", func() {
1433+
By("Setting the Synchronized condition to False")
1434+
Eventually(k.UpdateStatus(mapiMachine, func() {
1435+
mapiMachine.Status.Conditions = []mapiv1beta1.Condition{
1436+
{
1437+
Type: consts.SynchronizedCondition,
1438+
Status: corev1.ConditionFalse,
1439+
Reason: "ErrorReason",
1440+
Message: "Error message",
1441+
LastTransitionTime: metav1.Now(),
1442+
},
1443+
}
1444+
})).Should(Succeed())
1445+
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 the Synchronized condition is not true means changes may not take effect")))
1456+
}, timeout).Should(Succeed())
1457+
})
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
1475+
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 the Synchronized condition is not true 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 the Synchronized condition is not true means changes may not take effect")))
1501+
}, timeout).Should(Succeed())
1502+
})
1503+
})
1504+
13671505
})
13681506
})
13691507

0 commit comments

Comments
 (0)