-
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.
- Loading branch information
1 parent
343128b
commit 9955bae
Showing
12 changed files
with
791 additions
and
18 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,111 @@ | ||
// 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 | ||
} | ||
|
||
// TODO: doc | ||
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 | ||
} |
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.