Skip to content

Commit

Permalink
Handle situations when operators were preinstalled
Browse files Browse the repository at this point in the history
The general plan is to continue adding `handle*` functions for the other
resources that an OperatorPolicy needs to examine. Each handle function
will update the policy's status (with conditions and relatedObjects),
and possibly emit compliance events. This may cause more compliance
events "than usual" compared to other controllers, but I think the
separation of concerns will help each function be more maintainable.

My hope is that some of the `*Cond` and `*Obj` functions in the status
section can be reused in the future handlers. There was already overlap
between the Subscription and OperatorGroup, so this seemed reasonable.

To check if the Subscription/OperatorGroup on the cluster matches what
is desired by the policy, a function `handleKeys` was created that
can be used for OperatorPolicies and ConfigurationPolicies.

Refs:
 - https://issues.redhat.com/browse/ACM-9283

Signed-off-by: Justin Kulikauskas <jkulikau@redhat.com>
  • Loading branch information
JustinKuli committed Jan 30, 2024
1 parent 2978f4e commit c358a12
Show file tree
Hide file tree
Showing 17 changed files with 1,476 additions and 333 deletions.
17 changes: 17 additions & 0 deletions api/v1/configurationpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// A custom type is required since there is no way to have a kubebuilder marker
Expand Down Expand Up @@ -324,6 +325,22 @@ type ObjectResource struct {
Metadata ObjectMetadata `json:"metadata,omitempty"`
}

func ObjectResourceFromObj(obj client.Object) ObjectResource {
name := obj.GetName()
if name == "" {
name = "*"
}

return ObjectResource{
Kind: obj.GetObjectKind().GroupVersionKind().Kind,
APIVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
Metadata: ObjectMetadata{
Name: name,
Namespace: obj.GetNamespace(),
},
}
}

// ObjectMetadata contains the resource metadata for an object being processed by the policy
type ObjectMetadata struct {
// Name of the referent. More info:
Expand Down
25 changes: 25 additions & 0 deletions api/v1beta1/operatorpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,31 @@ type OperatorPolicyStatus struct {
RelatedObjects []policyv1.RelatedObject `json:"relatedObjects"`
}

func (status OperatorPolicyStatus) RelatedObjsOfKind(kind string) map[int]policyv1.RelatedObject {
objs := make(map[int]policyv1.RelatedObject)

for i, related := range status.RelatedObjects {
if related.Object.Kind == kind {
objs[i] = related
}
}

return objs
}

// Searches the conditions of the policy, and returns the index and condition matching the
// given condition Type. It will return -1 as the index if no condition of the specified
// Type is found.
func (status OperatorPolicyStatus) GetCondition(condType string) (int, metav1.Condition) {
for i, cond := range status.Conditions {
if cond.Type == condType {
return i, cond
}
}

return -1, metav1.Condition{}
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

Expand Down
132 changes: 75 additions & 57 deletions controllers/configurationpolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2586,72 +2586,28 @@ func (r *ConfigurationPolicyReconciler) checkAndUpdateResource(
res = r.TargetK8sDynamicClient.Resource(obj.gvr)
}

updateSucceeded = false
// Use a copy since some values can be directly assigned to mergedObj in handleSingleKey.
existingObjectCopy := obj.existingObj.DeepCopy()
removeFieldsForComparison(existingObjectCopy)

var statusMismatch bool

for key := range obj.desiredObj.Object {
isStatus := key == "status"

// use metadatacompliancetype to evaluate metadata if it is set
keyComplianceType := complianceType
if key == "metadata" && mdComplianceType != "" {
keyComplianceType = mdComplianceType
}

// check key for mismatch
errorMsg, keyUpdateNeeded, mergedObj, skipped := handleSingleKey(
key, obj.desiredObj, existingObjectCopy, keyComplianceType, !r.DryRunSupported,
)
if errorMsg != "" {
log.Info(errorMsg)

return true, errorMsg, true, false
}

if mergedObj == nil && skipped {
continue
}

// only look at labels and annotations for metadata - configurationPolicies do not update other metadata fields
if key == "metadata" {
// if it's not the right type, the map will be empty
mdMap, _ := mergedObj.(map[string]interface{})
throwSpecViolation, message, updateNeeded, statusMismatch := handleKeys(
obj.desiredObj, obj.existingObj, existingObjectCopy, complianceType, mdComplianceType, !r.DryRunSupported,
)
if message != "" {
return true, message, true, false
}

// if either isn't found, they'll just be empty
mergedAnnotations, _, _ := unstructured.NestedStringMap(mdMap, "annotations")
mergedLabels, _, _ := unstructured.NestedStringMap(mdMap, "labels")
if updateNeeded {
mismatchLog := "Detected value mismatch"

obj.existingObj.SetAnnotations(mergedAnnotations)
obj.existingObj.SetLabels(mergedLabels)
} else {
obj.existingObj.UnstructuredContent()[key] = mergedObj
// Add a configuration breadcrumb for users that might be looking in the logs for a diff
if objectT.RecordDiff != policyv1.RecordDiffLog {
mismatchLog += " (Diff disabled. To log the diff, " +
"set 'spec.object-tempates[].recordDiff' to 'Log' for this object-template.)"
}

if keyUpdateNeeded {
if isStatus {
throwSpecViolation = true
statusMismatch = true

log.Info("Ignoring an update to the object status", "key", key)
} else {
updateNeeded = true
log.Info(mismatchLog)

mismatchLog := "Detected value mismatch for object key: " + key
// Add a configuration breadcrumb for users that might be looking in the logs for a diff
if objectT.RecordDiff != policyv1.RecordDiffLog {
mismatchLog += " (Diff disabled. To log the diff, " +
"set 'spec.object-tempates[].recordDiff' to 'Log' for this object-template.)"
}
log.Info(mismatchLog)
}
}
}

if updateNeeded {
// FieldValidation is supported in k8s 1.25 as beta release
// so if the version is below 1.25, we need to use client side validation to validate the object
if semver.Compare(r.serverVersion, "v1.25.0") < 0 {
Expand Down Expand Up @@ -2783,6 +2739,68 @@ func (r *ConfigurationPolicyReconciler) checkAndUpdateResource(
return throwSpecViolation, "", updateNeeded, updateSucceeded
}

// handleKeys goes through all of the fields in the desired object and checks if the existing object
// matches. When a field is a map or slice, the value in the existing object will be updated with
// the result of merging its current value with the desired value.
func handleKeys(
desiredObj unstructured.Unstructured,
existingObj *unstructured.Unstructured,
existingObjectCopy *unstructured.Unstructured,
compType string,
mdCompType string,
zeroValueEqualsNil bool,
) (throwSpecViolation bool, message string, updateNeeded bool, statusMismatch bool) {
for key := range desiredObj.Object {
isStatus := key == "status"

// use metadatacompliancetype to evaluate metadata if it is set
keyComplianceType := compType
if key == "metadata" && mdCompType != "" {
keyComplianceType = mdCompType
}

// check key for mismatch
errorMsg, keyUpdateNeeded, mergedObj, skipped := handleSingleKey(
key, desiredObj, existingObjectCopy, keyComplianceType, zeroValueEqualsNil,
)
if errorMsg != "" {
log.Info(errorMsg)

return true, errorMsg, true, statusMismatch
}

if mergedObj == nil && skipped {
continue
}

// only look at labels and annotations for metadata - configurationPolicies do not update other metadata fields
if key == "metadata" {
// if it's not the right type, the map will be empty
mdMap, _ := mergedObj.(map[string]interface{})

// if either isn't found, they'll just be empty
mergedAnnotations, _, _ := unstructured.NestedStringMap(mdMap, "annotations")
mergedLabels, _, _ := unstructured.NestedStringMap(mdMap, "labels")

existingObj.SetAnnotations(mergedAnnotations)
existingObj.SetLabels(mergedLabels)
} else {
existingObj.UnstructuredContent()[key] = mergedObj
}

if keyUpdateNeeded {
if isStatus {
throwSpecViolation = true
statusMismatch = true
} else {
updateNeeded = true
}
}
}

return
}

func removeFieldsForComparison(obj *unstructured.Unstructured) {
unstructured.RemoveNestedField(obj.Object, "metadata", "managedFields")
unstructured.RemoveNestedField(
Expand Down
Loading

0 comments on commit c358a12

Please sign in to comment.