Skip to content

Commit 003f701

Browse files
Merge pull request #690 from yuumasato/handle-deprecated-profiles
CMP-3149: Handle profile deprecation
2 parents 5459d12 + 76771f0 commit 003f701

File tree

9 files changed

+717
-252
lines changed

9 files changed

+717
-252
lines changed

pkg/apis/compliance/v1alpha1/profilebundle_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const ProfileBundleOwnerLabel = "compliance.openshift.io/profile-bundle"
1616
// ProfileImageDigestAnnotation is the parsed out digest of the content image
1717
const ProfileImageDigestAnnotation = "compliance.openshift.io/image-digest"
1818

19+
// ProfileStatusAnnotation is the parsed out status from the data stream
20+
const ProfileStatusAnnotation = "compliance.openshift.io/profile-status"
21+
1922
// DataStreamStatusType is the type for the data stream status
2023
type DataStreamStatusType string
2124

pkg/controller/compliancescan/compliancescan_controller.go

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/ComplianceAsCode/compliance-operator/pkg/controller/common"
1515
"github.com/ComplianceAsCode/compliance-operator/pkg/controller/metrics"
1616
"github.com/ComplianceAsCode/compliance-operator/pkg/utils"
17+
"github.com/ComplianceAsCode/compliance-operator/pkg/xccdf"
1718
"github.com/go-logr/logr"
1819
corev1 "k8s.io/api/core/v1"
1920
"k8s.io/apimachinery/pkg/api/errors"
@@ -282,8 +283,106 @@ func (r *ReconcileComplianceScan) validate(instance *compv1alpha1.ComplianceScan
282283
return true, nil
283284
}
284285

286+
func (r *ReconcileComplianceScan) notifyUseOfDeprecatedProfile(instance *compv1alpha1.ComplianceScan, logger logr.Logger) error {
287+
profile := &compv1alpha1.Profile{}
288+
tp := &compv1alpha1.TailoredProfile{}
289+
var profileName string
290+
291+
if instance.Spec.TailoringConfigMap != nil {
292+
// The ComplianceScan references a TailoredProfile
293+
tpName := xccdf.GetNameFromXCCDFTailoredProfileID(instance.Spec.Profile)
294+
295+
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: tpName, Namespace: common.GetComplianceOperatorNamespace()}, tp); err != nil {
296+
logger.Error(err, "Could not get TailoredProfile", "TailoredProfile", tpName, "ComplianceScan", instance.Name)
297+
return err
298+
}
299+
if tp.GetAnnotations()[compv1alpha1.ProfileStatusAnnotation] == "deprecated" {
300+
logger.Info("ComplianceScan is running with a deprecated tailored profile", instance.Name, tp.Name)
301+
r.Recorder.Eventf(
302+
instance, corev1.EventTypeWarning, "DeprecatedTailoredProfile",
303+
"TailoredProfile %s is deprecated and should be avoided. "+
304+
"Please consider using another profile", tp.Name)
305+
return nil
306+
}
307+
308+
if tp.Spec.Extends == "" {
309+
return nil
310+
}
311+
// The extends field references a profile by its full name
312+
profileName = tp.Spec.Extends
313+
} else {
314+
var pbName string
315+
pbs := &compv1alpha1.ProfileBundleList{}
316+
317+
// We first find the ProfileBundle matching the scan's spec, then we get the profile that matches the scan's Profile ID.
318+
// We do this because a profile ID is not unique across all PBs, but is unique withing a PB.
319+
// We can, but should not, infer the Profile name based on the ComplianceScan name.
320+
// Advanced users still could create Suites and Scans with arbitrary names, and that is exactly what we do in our tests.
321+
if err := r.Client.List(context.TODO(), pbs, client.InNamespace(common.GetComplianceOperatorNamespace())); err != nil {
322+
logger.Error(err, "Could not list ProfileBundles")
323+
return err
324+
}
325+
for _, pb := range pbs.Items {
326+
if pb.Spec.ContentFile == instance.Spec.Content && pb.Spec.ContentImage == instance.Spec.ContentImage {
327+
pbName = pb.Name
328+
break
329+
}
330+
}
331+
if pbName == "" {
332+
err := goerrors.New("Could not find ProfileBundle used by scan")
333+
logger.Error(err, "ComplianceScan uses non-existent ProfileBundle", "ComplianceScan", instance.Name, "Profile", instance.Spec.Profile)
334+
return err
335+
}
336+
337+
xccdfProfileName := xccdf.GetProfileNameFromID(instance.Spec.Profile)
338+
339+
// This is the full profile name,
340+
// taking into account the possiblity of an 'upstream-' prefix in the PB.
341+
profileName = pbName + "-" + xccdfProfileName
342+
}
343+
344+
if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: profileName, Namespace: common.GetComplianceOperatorNamespace()}, profile); err != nil {
345+
logger.Error(err, "Could not get Profile", "profile", profileName, "ns", common.GetComplianceOperatorNamespace())
346+
return err
347+
}
348+
349+
if profile.GetAnnotations()[compv1alpha1.ProfileStatusAnnotation] == "deprecated" {
350+
logger.Info("ComplianceScan is running with a deprecated profile", instance.Name, profile.Name)
351+
if instance.Spec.TailoringConfigMap != nil {
352+
r.Recorder.Eventf(
353+
instance, corev1.EventTypeWarning, "DeprecatedTailoredProfile",
354+
"TailoredProfile %s is extending a deprecated Profile (%s) that will be removed in a future version of Compliance Operator. "+
355+
"Please consider using a newer version of this profile", tp.Name, profile.Name)
356+
} else {
357+
r.Recorder.Eventf(
358+
instance, corev1.EventTypeWarning, "DeprecatedProfile",
359+
"Profile %s is deprecated and will be removed in a future version of Compliance Operator. "+
360+
"Please consider using a newer version of this profile", profile.Name)
361+
}
362+
}
363+
return nil
364+
}
365+
285366
func (r *ReconcileComplianceScan) phasePendingHandler(instance *compv1alpha1.ComplianceScan, logger logr.Logger) (reconcile.Result, error) {
286367
logger.Info("Phase: Pending")
368+
369+
err := r.notifyUseOfDeprecatedProfile(instance, logger)
370+
if err != nil {
371+
logger.Error(err, "Could not check whether the Profile used by ComplianceScan is deprecated", "ComplianceScan", instance.Name)
372+
instanceCopy := instance.DeepCopy()
373+
instanceCopy.Status.Phase = compv1alpha1.PhaseDone
374+
instanceCopy.Status.Result = compv1alpha1.ResultError
375+
instanceCopy.Status.ErrorMessage = "Could not check whether the Profile used by ComplianceScan is deprecated"
376+
instanceCopy.Status.StartTimestamp = &metav1.Time{Time: time.Now()}
377+
instanceCopy.Status.EndTimestamp = &metav1.Time{Time: time.Now()}
378+
err = r.Client.Status().Update(context.TODO(), instanceCopy)
379+
if err != nil {
380+
logger.Error(err, "Cannot update the status")
381+
return reconcile.Result{}, err
382+
}
383+
return reconcile.Result{}, nil
384+
}
385+
287386
// Remove annotation if needed
288387
if instance.NeedsRescan() {
289388
instanceCopy := instance.DeepCopy()
@@ -307,7 +406,7 @@ func (r *ReconcileComplianceScan) phasePendingHandler(instance *compv1alpha1.Com
307406
instance.Status.Result = compv1alpha1.ResultNotAvailable
308407
instance.Status.StartTimestamp = &metav1.Time{Time: time.Now()}
309408
instance.Status.EndTimestamp = nil
310-
err := r.Client.Status().Update(context.TODO(), instance)
409+
err = r.Client.Status().Update(context.TODO(), instance)
311410
if err != nil {
312411
logger.Error(err, "Cannot update the status")
313412
return reconcile.Result{}, err

pkg/controller/compliancescan/compliancescan_controller_test.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ var _ = Describe("Testing compliancescan controller phases", func() {
137137
Size: compv1alpha1.DefaultRawStorageSize,
138138
},
139139
},
140+
ContentImage: "MyContentImage",
141+
Content: "MyContentFile",
142+
Profile: "xccdf_org.ssgproject.content_profile_unmoderate",
140143
},
141144
}
142145
platformscaninstance = &compv1alpha1.ComplianceScan{
@@ -204,12 +207,44 @@ var _ = Describe("Testing compliancescan controller phases", func() {
204207
},
205208
}
206209

207-
objs = append(objs, nodeinstance1, nodeinstance2, caSecret, serverSecret, clientSecret, ns)
210+
profileList := &compv1alpha1.ProfileList{}
211+
profile := &compv1alpha1.Profile{
212+
TypeMeta: metav1.TypeMeta{
213+
Kind: "Profile",
214+
APIVersion: compv1alpha1.SchemeGroupVersion.String(),
215+
},
216+
ObjectMeta: metav1.ObjectMeta{
217+
Name: "some-product-unmoderate",
218+
Namespace: common.GetComplianceOperatorNamespace(),
219+
},
220+
ProfilePayload: compv1alpha1.ProfilePayload{
221+
Title: "Unmoderate setteings",
222+
Description: "Describes obnoxious recommendations",
223+
ID: "xccdf_org.ssgproject.content_profile_unmoderate",
224+
},
225+
}
226+
profileBundleList := &compv1alpha1.ProfileBundleList{}
227+
profileBundle := &compv1alpha1.ProfileBundle{
228+
TypeMeta: metav1.TypeMeta{
229+
Kind: "ProfileBundle",
230+
APIVersion: compv1alpha1.SchemeGroupVersion.String(),
231+
},
232+
ObjectMeta: metav1.ObjectMeta{
233+
Name: "some-product",
234+
Namespace: common.GetComplianceOperatorNamespace(),
235+
},
236+
Spec: compv1alpha1.ProfileBundleSpec{
237+
ContentImage: "MyContentImage",
238+
ContentFile: "MyContentFile",
239+
},
240+
}
241+
242+
objs = append(objs, nodeinstance1, nodeinstance2, caSecret, serverSecret, clientSecret, ns, profile, profileBundle)
208243
scheme := scheme.Scheme
209-
scheme.AddKnownTypes(compv1alpha1.SchemeGroupVersion, compliancescaninstance, results)
244+
scheme.AddKnownTypes(compv1alpha1.SchemeGroupVersion, compliancescaninstance, results, profile, profileList, profileBundle, profileBundleList)
210245

211246
statusObjs := []runtimeclient.Object{}
212-
statusObjs = append(statusObjs, compliancescaninstance)
247+
statusObjs = append(statusObjs, compliancescaninstance, profile)
213248

214249
client := fake.NewClientBuilder().
215250
WithScheme(scheme).

pkg/controller/tailoredprofile/tailoredprofile_controller.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ func (r *ReconcileTailoredProfile) Reconcile(ctx context.Context, request reconc
169169
return r.setOwnership(tpCopy, p)
170170
}
171171

172+
// Warn that TailoredProfile extends deprecated Profile
173+
if p.GetAnnotations()[cmpv1alpha1.ProfileStatusAnnotation] == "deprecated" {
174+
reqLogger.Info("TailoredProfile extends a deprecated profile", instance.Name, p.Name)
175+
r.Recorder.Eventf(
176+
instance, corev1.EventTypeWarning, "DeprecatedTailoredProfile",
177+
"TailoredProfile %s is extending a deprecated Profile (%s) that will be removed in a future version of Compliance Operator. "+
178+
"Please consider using a newer version of this profile", instance.Name, p.Name)
179+
}
180+
172181
} else {
173182
if !isValidationRequired(instance) {
174183
// check if the TailoredProfile is empty without any extends

pkg/profileparser/profileparser.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ func parseProfileFromNode(profileRoot *xmlquery.Node, pb *cmpv1alpha1.ProfileBun
310310
if id == "" {
311311
return LogAndReturnError("no id in profile")
312312
}
313+
// Profile status is an optional field and can be nil
314+
status := profileObj.SelectElement("xccdf-1.2:status")
313315
title := profileObj.SelectElement("xccdf-1.2:title")
314316
if title == nil {
315317
return LogAndReturnError("no title in profile")
@@ -387,6 +389,7 @@ func parseProfileFromNode(profileRoot *xmlquery.Node, pb *cmpv1alpha1.ProfileBun
387389
}
388390

389391
annotateWithNonce(&p, nonce)
392+
annotateWithStatus(&p, status)
390393

391394
err := action(&p)
392395
if err != nil {
@@ -794,6 +797,17 @@ func annotateWithNonce(o metav1.Object, nonce string) {
794797
o.SetAnnotations(annotations)
795798
}
796799

800+
func annotateWithStatus(o metav1.Object, status *xmlquery.Node) {
801+
if status != nil {
802+
annotations := o.GetAnnotations()
803+
if annotations == nil {
804+
annotations = make(map[string]string)
805+
}
806+
annotations[cmpv1alpha1.ProfileStatusAnnotation] = status.InnerText()
807+
o.SetAnnotations(annotations)
808+
}
809+
}
810+
797811
type complianceStandard struct {
798812
Name string
799813
hrefMatcher *regexp.Regexp

pkg/xccdf/tailoring.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ func GetXCCDFProfileID(tp *cmpv1alpha1.TailoredProfile) string {
8888
return fmt.Sprintf("xccdf_%s_profile_%s", XCCDFNamespace, tp.Name)
8989
}
9090

91+
// GetNameFromXCCDFTailoredProfileID gets the name of a tailored profile
92+
// from the TailoredProfile ID
93+
func GetNameFromXCCDFTailoredProfileID(id string) string {
94+
return strings.TrimPrefix(id, fmt.Sprintf("xccdf_%s_profile_", XCCDFNamespace))
95+
}
96+
9197
// GetProfileNameFromID gets a profile name from the xccdf ID
9298
func GetProfileNameFromID(id string) string {
9399
trimedName := strings.TrimPrefix(id, profileIDPrefix)

tests/e2e/framework/common.go

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,28 @@ func (f *Framework) PrintROSADebugInfo(t *testing.T) {
176176
}
177177
}
178178

179+
func (f *Framework) CreateProfileBundle(pbName string, baselineImage string, contentFile string) (*compv1alpha1.ProfileBundle, error) {
180+
origPb := &compv1alpha1.ProfileBundle{
181+
ObjectMeta: metav1.ObjectMeta{
182+
Name: pbName,
183+
Namespace: f.OperatorNamespace,
184+
},
185+
Spec: compv1alpha1.ProfileBundleSpec{
186+
ContentImage: baselineImage,
187+
ContentFile: contentFile,
188+
},
189+
}
190+
// Pass nil in as the cleanupOptions since so we don't invoke all the
191+
// cleanup function code in Create. Use defer to cleanup the
192+
// ProfileBundle at the end of the test, instead of at the end of the
193+
// suite.
194+
log.Printf("Creating ProfileBundle %s", pbName)
195+
if err := f.Client.Create(context.TODO(), origPb, nil); err != nil {
196+
return nil, err
197+
}
198+
return origPb, nil
199+
}
200+
179201
func (f *Framework) cleanUpProfileBundle(p string) error {
180202
pb := &compv1alpha1.ProfileBundle{
181203
ObjectMeta: metav1.ObjectMeta{
@@ -449,6 +471,55 @@ func (f *Framework) ensureTestNamespaceExists() error {
449471

450472
}
451473

474+
func (f *Framework) WaitForProfileDeprecatedWarning(t *testing.T, scanName string, profileName string) error {
475+
polledScan := &compv1alpha1.ComplianceScan{}
476+
477+
// Wait for profile deprecation warning event
478+
err := wait.Poll(RetryInterval, Timeout, func() (bool, error) {
479+
getErr := f.Client.Get(context.TODO(), types.NamespacedName{Name: scanName, Namespace: f.OperatorNamespace}, polledScan)
480+
if getErr != nil {
481+
t.Log(getErr)
482+
return false, nil
483+
}
484+
485+
profileEventList, getEventErr := f.KubeClient.CoreV1().Events(f.OperatorNamespace).List(context.TODO(), metav1.ListOptions{
486+
FieldSelector: "reason=DeprecatedProfile",
487+
})
488+
if getEventErr != nil {
489+
t.Log(getEventErr)
490+
return false, nil
491+
}
492+
493+
tailoredProfileEventList, getEventErr := f.KubeClient.CoreV1().Events(f.OperatorNamespace).List(context.TODO(), metav1.ListOptions{
494+
FieldSelector: "reason=DeprecatedTailoredProfile",
495+
})
496+
if getEventErr != nil {
497+
t.Log(getEventErr)
498+
return false, nil
499+
}
500+
501+
re := regexp.MustCompile(fmt.Sprintf(".*%s.*", profileName))
502+
for _, item := range profileEventList.Items {
503+
if item.InvolvedObject.Name == polledScan.Name && re.MatchString(item.Message) {
504+
t.Logf("Found ComplianceScan deprecated profile event: %s", item.Message)
505+
return true, nil
506+
}
507+
}
508+
for _, item := range tailoredProfileEventList.Items {
509+
if item.InvolvedObject.Name == polledScan.Name && re.MatchString(item.Message) {
510+
t.Logf("Found ComplianceScan deprecated profile event: %s", item.Message)
511+
return true, nil
512+
}
513+
}
514+
return false, nil
515+
})
516+
if err != nil {
517+
t.Fatalf("No ComplianceScan event for profile \"%s\" found", profileName)
518+
return err
519+
}
520+
return nil
521+
}
522+
452523
// waitForProfileBundleStatus will poll until the compliancescan that we're
453524
// lookingfor reaches a certain status, or until a timeout is reached.
454525
func (f *Framework) WaitForProfileBundleStatus(name string, status compv1alpha1.DataStreamStatusType) error {
@@ -1160,7 +1231,7 @@ func (f *Framework) AssertScanIsCompliant(name, namespace string) error {
11601231
return err
11611232
}
11621233
if cs.Status.Result != compv1alpha1.ResultCompliant {
1163-
return fmt.Errorf("scan result was %s instead of %s", compv1alpha1.ResultCompliant, cs.Status.Result)
1234+
return fmt.Errorf("scan result was %s instead of %s", cs.Status.Result, compv1alpha1.ResultCompliant)
11641235
}
11651236
return nil
11661237
}
@@ -1238,7 +1309,7 @@ func (f *Framework) AssertScanIsNonCompliant(name, namespace string) error {
12381309
return err
12391310
}
12401311
if cs.Status.Result != compv1alpha1.ResultNonCompliant {
1241-
return fmt.Errorf("scan result was %s instead of %s", compv1alpha1.ResultNonCompliant, cs.Status.Result)
1312+
return fmt.Errorf("scan result was %s instead of %s", cs.Status.Result, compv1alpha1.ResultNonCompliant)
12421313
}
12431314
return nil
12441315
}
@@ -1251,7 +1322,7 @@ func (f *Framework) AssertScanIsNotApplicable(name, namespace string) error {
12511322
return err
12521323
}
12531324
if cs.Status.Result != compv1alpha1.ResultNotApplicable {
1254-
return fmt.Errorf("scan result was %s instead of %s", compv1alpha1.ResultNotApplicable, cs.Status.Result)
1325+
return fmt.Errorf("scan result was %s instead of %s", cs.Status.Result, compv1alpha1.ResultNotApplicable)
12551326
}
12561327
return nil
12571328
}
@@ -1264,7 +1335,7 @@ func (f *Framework) AssertScanIsInError(name, namespace string) error {
12641335
return err
12651336
}
12661337
if cs.Status.Result != compv1alpha1.ResultError {
1267-
return fmt.Errorf("scan result was %s instead of %s", compv1alpha1.ResultError, cs.Status.Result)
1338+
return fmt.Errorf("scan result was %s instead of %s", cs.Status.Result, compv1alpha1.ResultError)
12681339
}
12691340
if cs.Status.ErrorMessage == "" {
12701341
return fmt.Errorf("scan 'errormsg' is empty, but it should be set")

0 commit comments

Comments
 (0)