-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Many details about the event emitted and functions that verify the form of the fields are taken from other policy repositories. Many of the helpful functions added to the toolkit are similarly not original, but are an evolution of functions used in other repositories' tests. Signed-off-by: Justin Kulikauskas <[email protected]>
- Loading branch information
1 parent
20756ed
commit f05d46b
Showing
14 changed files
with
988 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// 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" | ||
) | ||
|
||
// K8sEmitter is an emitter of Kubernetes events which the policy framework | ||
// watches for in order to aggregate and report policy status. | ||
type K8sEmitter struct { | ||
// Client is a Kubernetes client for the cluster where the compliance events | ||
// will be created. | ||
Client client.Client | ||
|
||
// Source contains optional information for where the event comes from. | ||
Source corev1.EventSource | ||
|
||
// Mutators modify the Event after the fields are initially set, but before | ||
// it is created on the cluster. They are run in the order they are defined. | ||
Mutators []func(corev1.Event) (corev1.Event, error) | ||
} | ||
|
||
// Emit creates the Kubernetes Event on the cluster. It returns an error if the | ||
// API call fails. | ||
func (e K8sEmitter) Emit(ctx context.Context, pl nucleusv1beta1.PolicyLike) error { | ||
_, err := e.EmitEvent(ctx, pl) | ||
|
||
return err | ||
} | ||
|
||
// EmitEvent creates the Kubernetes Event on the cluster. It returns the Event | ||
// that was (at least) attempted to be created, and an error if the API call | ||
// fails. | ||
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" | ||
} | ||
|
||
src := corev1.EventSource{ | ||
Component: e.Source.Component, | ||
Host: e.Source.Host, | ||
} | ||
|
||
// These fields are required for the event to function as expected | ||
if src.Component == "" { | ||
src.Component = "policy-nucleus-default" | ||
} | ||
|
||
if src.Host == "" { | ||
src.Host = "policy-nucleus-default" | ||
} | ||
|
||
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: src, | ||
FirstTimestamp: metav1.NewTime(time), | ||
LastTimestamp: metav1.NewTime(time), | ||
Count: 1, | ||
Type: evType, | ||
EventTime: metav1.NewMicroTime(time), | ||
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: src.Component, | ||
ReportingInstance: src.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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
// Copyright Contributors to the Open Cluster Management project | ||
|
||
package testutils | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
corev1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
) | ||
|
||
func TestObjNN(t *testing.T) { | ||
t.Parallel() | ||
|
||
tests := map[string]struct { | ||
inpObj client.Object | ||
wantName string | ||
wantNS string | ||
}{ | ||
"namespaced unstructured": { | ||
inpObj: &unstructured.Unstructured{Object: map[string]interface{}{ | ||
"metadata": map[string]interface{}{ | ||
"name": "foo", | ||
"namespace": "world", | ||
}, | ||
}}, | ||
wantName: "foo", | ||
wantNS: "world", | ||
}, | ||
"cluster-scoped unstructured": { | ||
inpObj: &unstructured.Unstructured{Object: map[string]interface{}{ | ||
"metadata": map[string]interface{}{ | ||
"name": "bar", | ||
}, | ||
}}, | ||
wantName: "bar", | ||
wantNS: "", | ||
}, | ||
"(namespaced) configmap": { | ||
inpObj: &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{ | ||
Name: "my-cm", | ||
Namespace: "kube-one", | ||
}}, | ||
wantName: "my-cm", | ||
wantNS: "kube-one", | ||
}, | ||
"(cluster-scoped) node": { | ||
inpObj: &corev1.Node{ObjectMeta: metav1.ObjectMeta{ | ||
Name: "unit-tests-only", | ||
}}, | ||
wantName: "unit-tests-only", | ||
wantNS: "", | ||
}, | ||
} | ||
|
||
for name, tcase := range tests { | ||
got := ObjNN(tcase.inpObj) | ||
|
||
if got.Name != tcase.wantName { | ||
t.Errorf("Wanted name '%v', got '%v' in test '%v'", tcase.wantName, got.Name, name) | ||
} | ||
|
||
if got.Namespace != tcase.wantNS { | ||
t.Errorf("Wanted namespace '%v', got '%v' in test '%v'", tcase.wantNS, got.Namespace, name) | ||
} | ||
} | ||
} | ||
|
||
func TestEventFilter(t *testing.T) { | ||
t.Parallel() | ||
|
||
now := time.Now() | ||
old := now.Add(-time.Minute) | ||
veryOld := now.Add(-time.Hour) | ||
|
||
sampleEvents := []corev1.Event{{ | ||
Message: "hello", | ||
Type: "Normal", | ||
EventTime: metav1.NewMicroTime(veryOld), | ||
}, { | ||
Message: "goodbye", | ||
Type: "Warning", | ||
EventTime: metav1.NewMicroTime(old), | ||
}, { | ||
Message: "carpe diem [", | ||
Type: "Normal", | ||
EventTime: metav1.NewMicroTime(now), | ||
}, { | ||
Message: "what time is it?", | ||
Type: "Warning", | ||
}} | ||
|
||
tests := map[string]struct { | ||
inpType string | ||
inpMsg string | ||
inpSince time.Time | ||
wantIdxs []int | ||
}{ | ||
"#NoFilter": { | ||
inpType: "", | ||
inpMsg: "", | ||
inpSince: time.Time{}, | ||
wantIdxs: []int{0, 1, 2, 3}, | ||
}, | ||
"recent events, plus the one with no time specified": { | ||
inpType: "", | ||
inpMsg: "", | ||
inpSince: now.Add(-5 * time.Minute), | ||
wantIdxs: []int{1, 2, 3}, | ||
}, | ||
"only warnings": { | ||
inpType: "Warning", | ||
inpMsg: "", | ||
inpSince: time.Time{}, | ||
wantIdxs: []int{1, 3}, | ||
}, | ||
"basic regex for a space": { | ||
inpType: "", | ||
inpMsg: ".* .*", | ||
inpSince: time.Time{}, | ||
wantIdxs: []int{2, 3}, | ||
}, | ||
"just a space": { | ||
inpType: "", | ||
inpMsg: " ", | ||
inpSince: time.Time{}, | ||
wantIdxs: []int{2, 3}, | ||
}, | ||
"invalid inescaped regex": { | ||
inpType: "", | ||
inpMsg: "[", | ||
inpSince: time.Time{}, | ||
wantIdxs: []int{2}, | ||
}, | ||
} | ||
|
||
for name, tcase := range tests { | ||
got := EventFilter(sampleEvents, tcase.inpType, tcase.inpMsg, tcase.inpSince) | ||
|
||
if len(got) != len(tcase.wantIdxs) { | ||
t.Fatalf("Expected %v events to be returned, got %v in test %v: got events: %v", | ||
len(tcase.wantIdxs), len(got), name, got) | ||
} | ||
|
||
for i, wantIdx := range tcase.wantIdxs { | ||
if sampleEvents[wantIdx].String() != got[i].String() { | ||
t.Errorf("Mismatch on item #%v in test %v. Expected '%v' got '%v'", | ||
i, name, sampleEvents[wantIdx], got[i]) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.