Skip to content

Commit

Permalink
compliance events WIP
Browse files Browse the repository at this point in the history
Signed-off-by: Justin Kulikauskas <[email protected]>
  • Loading branch information
JustinKuli committed May 17, 2024
1 parent 343128b commit af50a33
Show file tree
Hide file tree
Showing 9 changed files with 439 additions and 11 deletions.
24 changes: 24 additions & 0 deletions api/v1beta1/policycore_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,27 @@ type PolicyCore struct {
Spec PolicyCoreSpec `json:"spec,omitempty"`
Status PolicyCoreStatus `json:"status,omitempty"`
}

//+kubebuilder:object:generate=false

// PolicyLike is an interface that policies should implement so that they can be worked with
// generally, without worrying about the specific kind of policy.
type PolicyLike interface {
client.Object

// The ComplianceState (Compliant/NonCompliant) of the specific policy.
ComplianceState() ComplianceState

// A human-readable string describing the current state of the policy, and why it is either
// Compliant or NonCompliant.
ComplianceMessage() string

// The "parent" object on this cluster for the specific policy. Generally a Policy, in the API
// GroupVersion `policy.open-cluster-management.io/v1`. For namespaced kinds of policies, this
// will usually be the owner of the policy. For cluster-scoped policies, this must be stored
// some other way.
Parent() metav1.OwnerReference

// The namespace of the "parent" object.
ParentNamespace() string
}
110 changes: 110 additions & 0 deletions pkg/compliance/k8sEventEmitter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright Contributors to the Open Cluster Management project

package compliance

import (
"context"
"fmt"
"time"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

nucleusv1beta1 "open-cluster-management.io/governance-policy-nucleus/api/v1beta1"
)

// Can't be an interface because then Emit couldn't be a method
type K8sEmitter struct {
Client client.Client // TODO: required
Source corev1.EventSource // TODO: optional?

// TODO: debatable for inclusion; allows tweaks of the event like adding/removing labels
// if not included, we could skip creating the event, just build it for the user to send
// but if it is included, we can build bigger things on top of this, and still have extensibility
Mutators []func(corev1.Event) (corev1.Event, error)
}

// TODO: maybe this would be a good interface for multiple emitters?
func (e K8sEmitter) Emit(ctx context.Context, pl nucleusv1beta1.PolicyLike) error {
_, err := e.EmitEvent(ctx, pl)

return err
}

func (e K8sEmitter) EmitEvent(ctx context.Context, pl nucleusv1beta1.PolicyLike) (*corev1.Event, error) {
plGVK := pl.GetObjectKind().GroupVersionKind()
time := time.Now()

// This event name matches the convention of recorders from client-go
name := fmt.Sprintf("%v.%x", pl.Parent().Name, time.UnixNano())

// The reason must match a pattern looked for by the policy framework
var reason string
if ns := pl.GetNamespace(); ns != "" {
reason = "policy: " + ns + "/" + pl.GetName()
} else {
reason = "policy: " + pl.GetName()
}

// The message must begin with the compliance, then should go into a descriptive message
message := string(pl.ComplianceState()) + "; " + pl.ComplianceMessage()

evType := "Normal"
if pl.ComplianceState() != nucleusv1beta1.Compliant {
evType = "Warning"
}

event := corev1.Event{
TypeMeta: metav1.TypeMeta{
Kind: "Event",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: pl.ParentNamespace(),
Labels: pl.GetLabels(),
Annotations: pl.GetAnnotations(),
},
InvolvedObject: corev1.ObjectReference{
Kind: pl.Parent().Kind,
Namespace: pl.ParentNamespace(),
Name: pl.Parent().Name,
UID: pl.Parent().UID,
APIVersion: pl.Parent().APIVersion,
},
Reason: reason,
Message: message,
Source: e.Source,
FirstTimestamp: metav1.NewTime(time),
LastTimestamp: metav1.NewTime(time),
Count: 1,
Type: evType,
EventTime: metav1.NewMicroTime(time), // does this break anything?
Series: nil,
Action: "ComplianceStateUpdate",
Related: &corev1.ObjectReference{
Kind: plGVK.Kind,
Namespace: pl.GetNamespace(),
Name: pl.GetName(),
UID: pl.GetUID(),
APIVersion: plGVK.GroupVersion().String(),
ResourceVersion: pl.GetResourceVersion(),
},
ReportingController: e.Source.Component,
ReportingInstance: e.Source.Host,
}

for _, mutatorFunc := range e.Mutators {
var err error

event, err = mutatorFunc(event)
if err != nil {
return nil, err
}
}

err := e.Client.Create(ctx, &event)

return &event, err
}
42 changes: 42 additions & 0 deletions pkg/testutils/toolkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ package testutils
import (
"context"
"fmt"
"regexp"
"sort"

corev1 "k8s.io/api/core/v1"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
gomegaTypes "github.com/onsi/gomega/types"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

Expand Down Expand Up @@ -59,6 +63,44 @@ func (tk Toolkit) CleanlyCreate(ctx context.Context, obj client.Object) error {
return createErr
}

// This regular expression is copied from
// https://github.com/open-cluster-management-io/governance-policy-framework-addon/blob/v0.13.0/controllers/statussync/policy_status_sync.go#L220
var compEventRegex = regexp.MustCompile(`(?i)^policy:\s*(?:([a-z0-9.-]+)\s*\/)?(.+)`)

// GetComplianceEvents queries the cluster and returns a sorted list of the kubernetes compliance
// events for the given policy.
func (tk Toolkit) GetComplianceEvents(
ctx context.Context, ns string, parentUID types.UID, templateName string,
) ([]corev1.Event, error) {
list := &corev1.EventList{}

err := tk.List(ctx, list, client.InNamespace(ns))
if err != nil {
return nil, err
}

events := make([]corev1.Event, 0)

for _, event := range list.Items {
event := event

if event.InvolvedObject.UID != parentUID {
continue
}

submatch := compEventRegex.FindStringSubmatch(event.Reason)
if len(submatch) >= 3 && submatch[2] == templateName {
events = append(events, event)
}
}

sort.SliceStable(events, func(i, j int) bool {
return events[i].Name < events[j].Name
})

return events, nil
}

// EC runs assertions on asynchronous behavior, both *E*ventually and *C*onsistently,
// using the polling and timeout settings of the toolkit. Its usage should feel familiar
// to gomega users, simply skip the `.Should(...)` call and put your matcher as the second
Expand Down
35 changes: 33 additions & 2 deletions test/fakepolicy/api/v1beta1/fakepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import (
type FakePolicySpec struct {
nucleusv1beta1.PolicyCoreSpec `json:",inline"`

// targetConfigMaps defines the ConfigMaps which should be examined by this policy
// TargetConfigMaps defines the ConfigMaps which should be examined by this policy
TargetConfigMaps nucleusv1beta1.Target `json:"targetConfigMaps,omitempty"`

// targetUsingReflection defines whether to use reflection to find the ConfigMaps
// TargetUsingReflection defines whether to use reflection to find the ConfigMaps
TargetUsingReflection bool `json:"targetUsingReflection,omitempty"`

// DesiredConfigMapName - if this name is found, the policy will be compliant
DesiredConfigMapName string `json:"desiredConfigMapName,omitempty"`
}

//+kubebuilder:validation:Optional
Expand All @@ -41,6 +44,34 @@ type FakePolicy struct {
Status FakePolicyStatus `json:"status,omitempty"`
}

// ensure FakePolicy implements PolicyLike
var _ nucleusv1beta1.PolicyLike = (*FakePolicy)(nil)

func (f FakePolicy) ComplianceState() nucleusv1beta1.ComplianceState {
return f.Status.ComplianceState
}

func (f FakePolicy) ComplianceMessage() string {
idx, compCond := f.Status.GetCondition("Compliant")
if idx == -1 {
return ""
}

return compCond.Message
}

func (f FakePolicy) Parent() metav1.OwnerReference {
if len(f.OwnerReferences) == 0 {
return metav1.OwnerReference{}
}

return f.OwnerReferences[0]
}

func (f FakePolicy) ParentNamespace() string {
return f.Namespace
}

//+kubebuilder:object:root=true

// FakePolicyList contains a list of FakePolicy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ spec:
spec:
description: FakePolicySpec defines the desired state of FakePolicy
properties:
desiredConfigMapName:
description: DesiredConfigMapName - if this name is found, the policy
will be compliant
type: string
namespaceSelector:
description: |-
NamespaceSelector indicates which namespaces on the cluster this policy
Expand Down Expand Up @@ -130,7 +134,7 @@ spec:
- Critical
type: string
targetConfigMaps:
description: targetConfigMaps defines the ConfigMaps which should
description: TargetConfigMaps defines the ConfigMaps which should
be examined by this policy
properties:
exclude:
Expand Down Expand Up @@ -196,7 +200,7 @@ spec:
type: object
x-kubernetes-map-type: atomic
targetUsingReflection:
description: targetUsingReflection defines whether to use reflection
description: TargetUsingReflection defines whether to use reflection
to find the ConfigMaps
type: boolean
type: object
Expand Down
8 changes: 6 additions & 2 deletions test/fakepolicy/config/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ spec:
spec:
description: FakePolicySpec defines the desired state of FakePolicy
properties:
desiredConfigMapName:
description: DesiredConfigMapName - if this name is found, the policy
will be compliant
type: string
namespaceSelector:
description: |-
NamespaceSelector indicates which namespaces on the cluster this policy
Expand Down Expand Up @@ -139,7 +143,7 @@ spec:
- Critical
type: string
targetConfigMaps:
description: targetConfigMaps defines the ConfigMaps which should
description: TargetConfigMaps defines the ConfigMaps which should
be examined by this policy
properties:
exclude:
Expand Down Expand Up @@ -205,7 +209,7 @@ spec:
type: object
x-kubernetes-map-type: atomic
targetUsingReflection:
description: targetUsingReflection defines whether to use reflection
description: TargetUsingReflection defines whether to use reflection
to find the ConfigMaps
type: boolean
type: object
Expand Down
Loading

0 comments on commit af50a33

Please sign in to comment.