Skip to content

Commit

Permalink
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 9, 2024
1 parent 7ea4fed commit 1ecbc80
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ ENVTEST_K8S_VERSION ?= 1.29
.PHONY: test
test: manifests generate $(GINKGO) $(ENVTEST) ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" $(GINKGO) \
--coverpkg=./... --covermode=count --coverprofile=cover.out ./...
--coverpkg=./... --covermode=count --coverprofile=cover.out --github-output ./...

.PHONY: fuzz-test
fuzz-test:
Expand Down
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
// generically, 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
}
103 changes: 103 additions & 0 deletions pkg/compliance/k8sEventEmitter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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 // required
Source corev1.EventSource // optional?

// 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)
}

func (e K8sEmitter) Emit(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 go into the 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
}
55 changes: 55 additions & 0 deletions pkg/testutils/toolkit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright Contributors to the Open Cluster Management project

package testutils

import (
"context"
"regexp"
"sort"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type Toolkit struct {
client.Client
}

// 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 submatch != nil && submatch[2] == templateName {

Check failure on line 45 in pkg/testutils/toolkit.go

View workflow job for this annotation

GitHub Actions / golangci-lint

weakCond: suspicious `submatch != nil && submatch[2] == templateName`; nil check may not be enough, check for len (gocritic)
events = append(events, event)
}
}

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

return events, nil
}
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
45 changes: 41 additions & 4 deletions test/fakepolicy/controllers/fakepolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

nucleusv1alpha1 "open-cluster-management.io/governance-policy-nucleus/api/v1alpha1"
nucleusv1beta1 "open-cluster-management.io/governance-policy-nucleus/api/v1beta1"
"open-cluster-management.io/governance-policy-nucleus/pkg/compliance"
fakev1beta1 "open-cluster-management.io/governance-policy-nucleus/test/fakepolicy/api/v1beta1"
)

Expand Down Expand Up @@ -55,18 +56,48 @@ func (r *FakePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err
}

r.doSelections(ctx, policy)
cmFound := r.doSelections(ctx, policy)

policy.Status.SelectionComplete = true

complianceCondition := metav1.Condition{
Type: "Compliant",
Status: metav1.ConditionTrue,
Reason: "Found",
Message: "the desired configmap was found",
}

policy.Status.ComplianceState = nucleusv1beta1.Compliant

if !cmFound {
complianceCondition.Status = metav1.ConditionFalse
complianceCondition.Reason = "NotFound"
complianceCondition.Message = "the desired configmap was missing"
policy.Status.ComplianceState = nucleusv1beta1.NonCompliant
}

policy.Status.UpdateCondition(complianceCondition)

if err := r.Status().Update(ctx, policy); err != nil {
log.Error(err, "Failed to update status")

return ctrl.Result{}, err
}

return ctrl.Result{}, nil
emitter := compliance.K8sEmitter{
Client: r.Client,
Source: corev1.EventSource{
Component: "hello",
Host: "world",
},
}

_, err := emitter.Emit(ctx, policy)

return ctrl.Result{}, err
}

func (r *FakePolicyReconciler) doSelections(ctx context.Context, policy *fakev1beta1.FakePolicy) {
func (r *FakePolicyReconciler) doSelections(ctx context.Context, policy *fakev1beta1.FakePolicy) bool {
log := log.FromContext(ctx)

nsCond := metav1.Condition{
Expand Down Expand Up @@ -138,6 +169,8 @@ func (r *FakePolicyReconciler) doSelections(ctx context.Context, policy *fakev1b
list = &configMapResList{}
}

configMapFound := false

clientMatchedCMs, err := policy.Spec.TargetConfigMaps.GetMatches(ctx, r.Client, list)
if err != nil {
log.Error(err, "Failed to GetMatches for the TargetConfigMaps",
Expand All @@ -150,6 +183,10 @@ func (r *FakePolicyReconciler) doSelections(ctx context.Context, policy *fakev1b
clientCMs := make([]string, len(clientMatchedCMs))
for i, cm := range dynamicMatchedCMs {
clientCMs[i] = cm.GetNamespace() + "/" + cm.GetName()

if cm.GetName() == policy.Spec.DesiredConfigMapName {
configMapFound = true
}
}

slices.Sort(clientCMs)
Expand All @@ -159,7 +196,7 @@ func (r *FakePolicyReconciler) doSelections(ctx context.Context, policy *fakev1b

policy.Status.UpdateCondition(clientCond)

policy.Status.SelectionComplete = true
return configMapFound
}

type configMapResList struct {
Expand Down
3 changes: 3 additions & 0 deletions test/fakepolicy/test/complianceEvent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Copyright Contributors to the Open Cluster Management project

package test
Loading

0 comments on commit 1ecbc80

Please sign in to comment.