Skip to content

Commit 8cc5f7b

Browse files
committed
OCPBUGS-11228: Check for duplicate variables in TailoredProfile
Implement duplicate detection for setValues in TailoredProfile and add warning event [OCPBUGS-11228](https://issues.redhat.com/browse/OCPBUGS-11228)
1 parent fed54b4 commit 8cc5f7b

File tree

2 files changed

+118
-8
lines changed

2 files changed

+118
-8
lines changed

pkg/controller/tailoredprofile/tailoredprofile_controller.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -527,14 +527,37 @@ func (r *ReconcileTailoredProfile) getRulesFromSelections(tp *cmpv1alpha1.Tailor
527527
}
528528
return rules, nil
529529
}
530-
531530
func (r *ReconcileTailoredProfile) getVariablesFromSelections(tp *cmpv1alpha1.TailoredProfile, pb *cmpv1alpha1.ProfileBundle) ([]*cmpv1alpha1.Variable, error) {
532-
variableList := []*cmpv1alpha1.Variable{}
533-
for _, setValues := range tp.Spec.SetValues {
531+
// 1) First pass over tp.Spec.SetValues to detect duplicates, warn, and decide which value to keep
532+
// We choose to keep the last occurrence. The map "duplicates" maps variableName -> finalValue.
533+
// The slice "uniqueVarOrder" tracks the first time we saw the variable (for stable iteration).
534+
duplicates := make(map[string]string)
535+
uniqueVarOrder := []string{}
536+
537+
for i, setValue := range tp.Spec.SetValues {
538+
if oldVal, seen := duplicates[setValue.Name]; seen {
539+
// We found a duplicate. Warn that we will ignore the previous usage.
540+
r.Eventf(tp, corev1.EventTypeWarning,
541+
"DuplicatedSetValue",
542+
"Variable '%s' appears multiple times in setValues. The operator will keep the last usage with value '%s' (position %d), ignoring the previous value '%s'. This will fail in a future release. Please remove duplicates from setValues.",
543+
setValue.Name, setValue.Value, i, oldVal)
544+
} else {
545+
// Only record order the first time we see the variable
546+
uniqueVarOrder = append(uniqueVarOrder, setValue.Name)
547+
}
548+
// Overwrite with the newest/last usage
549+
duplicates[setValue.Name] = setValue.Value
550+
}
551+
552+
// 2) Second pass to actually retrieve the Variables and set their final values
553+
variableList := make([]*cmpv1alpha1.Variable, 0, len(uniqueVarOrder))
554+
for _, varName := range uniqueVarOrder {
555+
finalValue := duplicates[varName]
556+
557+
// Grab the Variable from the cluster
534558
variable := &cmpv1alpha1.Variable{}
535-
varKey := types.NamespacedName{Name: setValues.Name, Namespace: tp.Namespace}
536-
err := r.Client.Get(context.TODO(), varKey, variable)
537-
if err != nil {
559+
varKey := types.NamespacedName{Name: varName, Namespace: tp.Namespace}
560+
if err := r.Client.Get(context.TODO(), varKey, variable); err != nil {
538561
if kerrors.IsNotFound(err) {
539562
return nil, common.NewNonRetriableCtrlError("fetching variable: %w", err)
540563
}
@@ -548,8 +571,7 @@ func (r *ReconcileTailoredProfile) getVariablesFromSelections(tp *cmpv1alpha1.Ta
548571
}
549572

550573
// try setting the variable, this also validates the value
551-
err = variable.SetValue(setValues.Value)
552-
if err != nil {
574+
if err := variable.SetValue(finalValue); err != nil {
553575
return nil, common.NewNonRetriableCtrlError("setting variable: %s", err)
554576
}
555577

pkg/controller/tailoredprofile/tailoredprofile_controller_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,94 @@ var _ = Describe("TailoredprofileController", func() {
837837
})
838838
})
839839

840+
Context("with duplicated setValues for the same variable", func() {
841+
var tpName = "tailoring-duplicate-vars"
842+
843+
BeforeEach(func() {
844+
// Create a TailoredProfile with duplicate entries for "var-1"
845+
// Note "var-1" is owned by pb1 in your test setup if i < 5
846+
tp := &compv1alpha1.TailoredProfile{
847+
ObjectMeta: metav1.ObjectMeta{
848+
Name: tpName,
849+
Namespace: namespace,
850+
},
851+
Spec: compv1alpha1.TailoredProfileSpec{
852+
// No extends here, so it picks up the PB from the first rule/variable it finds
853+
EnableRules: []compv1alpha1.RuleReferenceSpec{
854+
{
855+
Name: "rule-1",
856+
Rationale: "Ensure ownership detection picks pb1",
857+
},
858+
},
859+
SetValues: []compv1alpha1.VariableValueSpec{
860+
{
861+
Name: "var-1",
862+
Value: "1111",
863+
},
864+
{
865+
Name: "var-1",
866+
Value: "2222", // Duplicate #2
867+
},
868+
{
869+
Name: "var-2",
870+
Value: "3333",
871+
},
872+
{
873+
Name: "var-1",
874+
Value: "4444", // Duplicate #3 (final mention)
875+
},
876+
},
877+
},
878+
}
879+
880+
createErr := r.Client.Create(ctx, tp)
881+
Expect(createErr).To(BeNil())
882+
})
883+
884+
It("keeps the last mention of the duplicated variable and raises a warning", func() {
885+
tpKey := types.NamespacedName{
886+
Name: tpName,
887+
Namespace: namespace,
888+
}
889+
tpReq := reconcile.Request{}
890+
tpReq.Name = tpName
891+
tpReq.Namespace = namespace
892+
893+
By("Reconciling the TailoredProfile the first time (to set ownership)")
894+
_, err := r.Reconcile(context.TODO(), tpReq)
895+
Expect(err).To(BeNil())
896+
897+
By("Reconciling the TailoredProfile the second time (to process setValues)")
898+
_, err = r.Reconcile(context.TODO(), tpReq)
899+
Expect(err).To(BeNil())
900+
901+
By("Ensuring the TailoredProfile ends in the Ready state")
902+
tp := &compv1alpha1.TailoredProfile{}
903+
geterr := r.Client.Get(ctx, tpKey, tp)
904+
Expect(geterr).To(BeNil())
905+
Expect(tp.Status.State).To(Equal(compv1alpha1.TailoredProfileStateReady))
906+
Expect(tp.Status.ErrorMessage).To(BeEmpty())
907+
908+
By("Ensuring the final configMap uses the last mention of var-1 (4444)")
909+
cm := &corev1.ConfigMap{}
910+
cmKey := types.NamespacedName{
911+
Name: tp.Status.OutputRef.Name,
912+
Namespace: tp.Status.OutputRef.Namespace,
913+
}
914+
geterr = r.Client.Get(ctx, cmKey, cm)
915+
Expect(geterr).To(BeNil())
916+
data := cm.Data["tailoring.xml"]
917+
// The final mention for var-1 must be "4444" in the rendered tailoring
918+
Expect(data).To(ContainSubstring(`idref="var_1"`))
919+
Expect(data).To(ContainSubstring(`4444`))
920+
// Ensure that 2222 or 1111 never appear, verifying the earlier duplicates are discarded
921+
Expect(data).NotTo(ContainSubstring("1111"))
922+
Expect(data).NotTo(ContainSubstring("2222"))
923+
Expect(data).To(ContainSubstring(`idref="var_2"`))
924+
Expect(data).To(ContainSubstring(`3333`))
925+
})
926+
})
927+
840928
Context("with no rules nor variables", func() {
841929
BeforeEach(func() {
842930
tp := &compv1alpha1.TailoredProfile{

0 commit comments

Comments
 (0)