Skip to content

Commit cfcd702

Browse files
authored
Merge pull request #695 from Vincent056/ocp-11228
OCPBUGS-11228: Check for duplicate variables in TailoredProfile
2 parents 861e80e + 97e6fb5 commit cfcd702

File tree

4 files changed

+189
-8
lines changed

4 files changed

+189
-8
lines changed

pkg/controller/tailoredprofile/tailoredprofile_controller.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -536,14 +536,30 @@ 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+
// First pass: Build de-duplicated map of variables, keeping last occurrence when duplicates exist.
541+
variables := make(map[string]string)
542+
543+
for i, setValue := range tp.Spec.SetValues {
544+
if oldVal, seen := variables[setValue.Name]; seen {
545+
// We found a duplicate. Warn that we will ignore the previous usage.
546+
r.Eventf(tp, corev1.EventTypeWarning,
547+
"DuplicatedSetValue",
548+
"Variable '%s' appears multiple times in setValues. The operator will keep the last usage with value '%s' (position %d), ignoring the previous value '%s'. Specifying a variable multiple times using setValues will fail in a future release. Please remove duplicates from setValues as it introduces ambiguity.",
549+
setValue.Name, setValue.Value, i, oldVal)
550+
}
551+
// Always use the last occurrence's value
552+
variables[setValue.Name] = setValue.Value
553+
}
554+
555+
// Second pass: Retrieve Variables from cluster and set their values
556+
// Variables are processed in random order (map iteration)
557+
variableList := make([]*cmpv1alpha1.Variable, 0, len(variables))
558+
for varName, value := range variables {
559+
// Grab the Variable from the cluster
543560
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 {
561+
varKey := types.NamespacedName{Name: varName, Namespace: tp.Namespace}
562+
if err := r.Client.Get(context.TODO(), varKey, variable); err != nil {
547563
if kerrors.IsNotFound(err) {
548564
return nil, common.NewNonRetriableCtrlError("fetching variable: %w", err)
549565
}
@@ -557,8 +573,7 @@ func (r *ReconcileTailoredProfile) getVariablesFromSelections(tp *cmpv1alpha1.Ta
557573
}
558574

559575
// try setting the variable, this also validates the value
560-
err = variable.SetValue(setValues.Value)
561-
if err != nil {
576+
if err := variable.SetValue(value); err != nil {
562577
return nil, common.NewNonRetriableCtrlError("setting variable: %s", err)
563578
}
564579

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=DuplicatedSetValue",
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
@@ -839,6 +839,48 @@ func TestScanTailoredProfileIsDeprecated(t *testing.T) {
839839
}
840840
}
841841

842+
func TestScanTailoredProfileHasDuplicateVariables(t *testing.T) {
843+
t.Parallel()
844+
f := framework.Global
845+
pbName := framework.GetObjNameFromTest(t)
846+
prefixName := func(profName, ruleBaseName string) string { return profName + "-" + ruleBaseName }
847+
varName := prefixName(pbName, "var-openshift-audit-profile")
848+
tpName := "test-tailored-profile-has-duplicate-variables"
849+
tp := &compv1alpha1.TailoredProfile{
850+
ObjectMeta: metav1.ObjectMeta{
851+
Name: tpName,
852+
Namespace: f.OperatorNamespace,
853+
},
854+
Spec: compv1alpha1.TailoredProfileSpec{
855+
Extends: "ocp4-cis",
856+
Title: "TestScanTailoredProfileIsDuplicateVariables",
857+
Description: "TestScanTailoredProfileIsDuplicateVariables",
858+
SetValues: []compv1alpha1.VariableValueSpec{
859+
{
860+
Name: varName,
861+
Rationale: "Value to be set",
862+
Value: "WriteRequestBodies",
863+
},
864+
{
865+
Name: varName,
866+
Rationale: "Value to be set",
867+
Value: "SomethingElse",
868+
},
869+
},
870+
},
871+
}
872+
err := f.Client.Create(context.TODO(), tp, nil)
873+
if err != nil {
874+
t.Fatal(err)
875+
}
876+
defer f.Client.Delete(context.TODO(), tp)
877+
// let's check if the profile is created and if event warning is being generated
878+
if err = f.WaitForDuplicatedVariableWarning(t, tpName, varName); err != nil {
879+
t.Fatal(err)
880+
}
881+
882+
}
883+
842884
func TestScanProducesRemediations(t *testing.T) {
843885
t.Parallel()
844886
f := framework.Global

0 commit comments

Comments
 (0)