Skip to content

Commit 1bae2fc

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 003f701 commit 1bae2fc

File tree

4 files changed

+196
-8
lines changed

4 files changed

+196
-8
lines changed

pkg/controller/tailoredprofile/tailoredprofile_controller.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -536,14 +536,37 @@ func (r *ReconcileTailoredProfile) getRulesFromSelections(tp *cmpv1alpha1.Tailor
536536
}
537537
return rules, nil
538538
}
539-
540539
func (r *ReconcileTailoredProfile) getVariablesFromSelections(tp *cmpv1alpha1.TailoredProfile, pb *cmpv1alpha1.ProfileBundle) ([]*cmpv1alpha1.Variable, error) {
541-
variableList := []*cmpv1alpha1.Variable{}
542-
for _, setValues := range tp.Spec.SetValues {
540+
// 1) First pass over tp.Spec.SetValues to detect duplicates, warn, and decide which value to keep
541+
// We choose to keep the last occurrence. The map "duplicates" maps variableName -> finalValue.
542+
// The slice "uniqueVarOrder" tracks the first time we saw the variable (for stable iteration).
543+
duplicates := make(map[string]string)
544+
uniqueVarOrder := []string{}
545+
546+
for i, setValue := range tp.Spec.SetValues {
547+
if oldVal, seen := duplicates[setValue.Name]; seen {
548+
// We found a duplicate. Warn that we will ignore the previous usage.
549+
r.Eventf(tp, corev1.EventTypeWarning,
550+
"DuplicatedSetValue",
551+
"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.",
552+
setValue.Name, setValue.Value, i, oldVal)
553+
} else {
554+
// Only record order the first time we see the variable
555+
uniqueVarOrder = append(uniqueVarOrder, setValue.Name)
556+
}
557+
// Overwrite with the newest/last usage
558+
duplicates[setValue.Name] = setValue.Value
559+
}
560+
561+
// 2) Second pass to actually retrieve the Variables and set their final values
562+
variableList := make([]*cmpv1alpha1.Variable, 0, len(uniqueVarOrder))
563+
for _, varName := range uniqueVarOrder {
564+
finalValue := duplicates[varName]
565+
566+
// Grab the Variable from the cluster
543567
variable := &cmpv1alpha1.Variable{}
544-
varKey := types.NamespacedName{Name: setValues.Name, Namespace: tp.Namespace}
545-
err := r.Client.Get(context.TODO(), varKey, variable)
546-
if err != nil {
568+
varKey := types.NamespacedName{Name: varName, Namespace: tp.Namespace}
569+
if err := r.Client.Get(context.TODO(), varKey, variable); err != nil {
547570
if kerrors.IsNotFound(err) {
548571
return nil, common.NewNonRetriableCtrlError("fetching variable: %w", err)
549572
}
@@ -557,8 +580,7 @@ func (r *ReconcileTailoredProfile) getVariablesFromSelections(tp *cmpv1alpha1.Ta
557580
}
558581

559582
// try setting the variable, this also validates the value
560-
err = variable.SetValue(setValues.Value)
561-
if err != nil {
583+
if err := variable.SetValue(finalValue); err != nil {
562584
return nil, common.NewNonRetriableCtrlError("setting variable: %s", err)
563585
}
564586

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{

tests/e2e/framework/common.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,42 @@ func (f *Framework) WaitForProfileDeprecatedWarning(t *testing.T, scanName strin
520520
return nil
521521
}
522522

523+
func (f *Framework) WaitForDuplicatedVariableWarning(t *testing.T, tpName string, variableName string) error {
524+
polledTailoredProfile := &compv1alpha1.TailoredProfile{}
525+
526+
// Wait for profile deprecation warning event
527+
err := wait.Poll(RetryInterval, Timeout, func() (bool, error) {
528+
getErr := f.Client.Get(context.TODO(), types.NamespacedName{Name: tpName, Namespace: f.OperatorNamespace}, polledTailoredProfile)
529+
if getErr != nil {
530+
t.Log(getErr)
531+
return false, nil
532+
}
533+
534+
duplicateValuesEventList, getEventErr := f.KubeClient.CoreV1().Events(f.OperatorNamespace).List(context.TODO(), metav1.ListOptions{
535+
FieldSelector: "reason=DuplicateSetValue",
536+
})
537+
if getEventErr != nil {
538+
t.Log(getEventErr)
539+
return false, nil
540+
}
541+
542+
re := regexp.MustCompile(fmt.Sprintf(".*%s.*", variableName))
543+
544+
for _, item := range duplicateValuesEventList.Items {
545+
if item.InvolvedObject.Name == polledTailoredProfile.Name && re.MatchString(item.Message) {
546+
t.Logf("Found TailoredProfile duplicated variable event: %s", item.Message)
547+
return true, nil
548+
}
549+
}
550+
return false, nil
551+
})
552+
if err != nil {
553+
t.Fatalf("No TailoredProfile event for variable \"%s\" found", variableName)
554+
return err
555+
}
556+
return nil
557+
}
558+
523559
// waitForProfileBundleStatus will poll until the compliancescan that we're
524560
// lookingfor reaches a certain status, or until a timeout is reached.
525561
func (f *Framework) WaitForProfileBundleStatus(name string, status compv1alpha1.DataStreamStatusType) error {

tests/e2e/parallel/main_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,48 @@ func TestScanTailoredProfileIsDeprecated(t *testing.T) {
783783
}
784784
}
785785

786+
func TestScanTailoredProfileHasDuplicateVariables(t *testing.T) {
787+
t.Parallel()
788+
f := framework.Global
789+
pbName := framework.GetObjNameFromTest(t)
790+
prefixName := func(profName, ruleBaseName string) string { return profName + "-" + ruleBaseName }
791+
varName := prefixName(pbName, "var-openshift-audit-profile")
792+
tpName := "test-tailored-profile-has-duplicate-variables"
793+
tp := &compv1alpha1.TailoredProfile{
794+
ObjectMeta: metav1.ObjectMeta{
795+
Name: tpName,
796+
Namespace: f.OperatorNamespace,
797+
},
798+
Spec: compv1alpha1.TailoredProfileSpec{
799+
Extends: "ocp4-cis",
800+
Title: "TestScanTailoredProfileIsDuplicateVariables",
801+
Description: "TestScanTailoredProfileIsDuplicateVariables",
802+
SetValues: []compv1alpha1.VariableValueSpec{
803+
{
804+
Name: varName,
805+
Rationale: "Value to be set",
806+
Value: "WriteRequestBodies",
807+
},
808+
{
809+
Name: varName,
810+
Rationale: "Value to be set",
811+
Value: "SomethingElse",
812+
},
813+
},
814+
},
815+
}
816+
err := f.Client.Create(context.TODO(), tp, nil)
817+
if err != nil {
818+
t.Fatal(err)
819+
}
820+
defer f.Client.Delete(context.TODO(), tp)
821+
// let's check if the profile is created and if event warning is being generated
822+
if err = f.WaitForDuplicatedVariableWarning(t, tpName, varName); err != nil {
823+
t.Fatal(err)
824+
}
825+
826+
}
827+
786828
func TestScanProducesRemediations(t *testing.T) {
787829
t.Parallel()
788830
f := framework.Global

0 commit comments

Comments
 (0)