Skip to content

Commit

Permalink
(feat) allow selecting based on status
Browse files Browse the repository at this point in the history
  • Loading branch information
roshbhatia committed Feb 27, 2025
1 parent 1f9c49c commit a2cab39
Show file tree
Hide file tree
Showing 14 changed files with 1,754 additions and 50 deletions.
27 changes: 27 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
linters-settings:
errcheck:
check-type-assertions: true
check-blank: true
exclude-functions:
- (*k8s.io/client-go/tools/cache.SharedIndexInformer).AddEventHandler
- (*k8s.io/client-go/tools/cache.SharedIndexInformer).AddEventHandlerWithResyncPeriod

linters:
enable:
- errcheck
- gofmt
- govet
- gosimple
- ineffassign
- staticcheck
- unused
- misspell

issues:
exclude-rules:
- linters:
- errcheck
text: "AddEventHandler"
- linters:
- errcheck
text: "AddEventHandlerWithResyncPeriod"
1 change: 0 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Roadmap

- [ ] In addition to kubernetes events, expose the ability to select on a status
- [ ] Create helm chart
- [ ] Expose metrics and traces via OTEL
- [ ] Have EventTriggeredJob create a meta block (inherit xyz from parent, like annotations, namespace, etc.)
Expand Down
22 changes: 19 additions & 3 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/roshbhatia/kubevent/pkg/controller"
"github.com/roshbhatia/kubevent/pkg/util"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"
Expand All @@ -29,11 +30,26 @@ func main() {
klog.Fatalf("Error building kubernetes clientset: %s", err.Error())
}

dynamicClient, err := dynamic.NewForConfig(cfg)
if err != nil {
klog.Fatalf("Error building dynamic client: %s", err.Error())
}

stopCh := util.SetupSignalHandler()

// Create controllers
eventController := controller.NewEventController(kubeClient)

if err := eventController.Run(2, stopCh); err != nil {
klog.Fatalf("Error running controller: %s", err.Error())
statusController := controller.NewStatusController(kubeClient, dynamicClient)

// Run the event controller
go func() {
if err := eventController.Run(2, stopCh); err != nil {
klog.Fatalf("Error running event controller: %s", err.Error())
}
}()

// Run the status controller (blocking)
if err := statusController.Run(2, stopCh); err != nil {
klog.Fatalf("Error running status controller: %s", err.Error())
}
}
48 changes: 48 additions & 0 deletions deploy/samples/example-job-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,52 @@ spec:
- name: notification
image: busybox
command: ["sh", "-c", "echo 'Pod was restarted!' && sleep 5"]
restartPolicy: Never
---
apiVersion: kubevent.roshanbhatia.com/v1alpha1
kind: EventTriggeredJob
metadata:
name: pod-ready-notification
spec:
statusSelector:
resourceKind: "Pod"
namePattern: "*"
namespacePattern: "default"
labelSelector:
matchLabels:
app: myapp
conditions:
- type: "Ready"
status: "True"
jobTemplate:
spec:
template:
spec:
containers:
- name: notification
image: busybox
command: ["sh", "-c", "echo 'Pod is now Ready!' && sleep 5"]
restartPolicy: Never
---
apiVersion: kubevent.roshanbhatia.com/v1alpha1
kind: EventTriggeredJob
metadata:
name: deployment-available-notification
spec:
statusSelector:
resourceKind: "Deployment"
namePattern: "web-*"
conditions:
- type: "Available"
status: "True"
- type: "Progressing"
status: "False"
jobTemplate:
spec:
template:
spec:
containers:
- name: notification
image: busybox
command: ["sh", "-c", "echo 'Deployment is now Available!' && sleep 5"]
restartPolicy: Never
84 changes: 75 additions & 9 deletions pkg/apis/kubevent/v1alpha1/crd_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ import (
)

func TestCRDValidation(t *testing.T) {
// Create a valid template
validTemplate := &EventTriggeredJob{
// Create a valid template with EventSelector
validEventTemplate := &EventTriggeredJob{
TypeMeta: metav1.TypeMeta{
APIVersion: "kubevent.roshanbhatia.com/v1alpha1",
Kind: "EventTriggeredJob",
},
ObjectMeta: metav1.ObjectMeta{
Name: "valid-template",
Name: "valid-event-template",
Namespace: "default",
},
Spec: EventTriggeredJobSpec{
EventSelector: EventSelector{
EventSelector: &EventSelector{
ResourceKind: "Pod",
NamePattern: "test-*",
EventTypes: []string{"CREATE", "DELETE"},
Expand All @@ -47,13 +47,79 @@ func TestCRDValidation(t *testing.T) {
},
}

// Validate required fields
if validTemplate.Spec.EventSelector.ResourceKind == "" {
t.Errorf("ResourceKind is required but allowed to be empty")
// Create a valid template with StatusSelector
validStatusTemplate := &EventTriggeredJob{
TypeMeta: metav1.TypeMeta{
APIVersion: "kubevent.roshanbhatia.com/v1alpha1",
Kind: "EventTriggeredJob",
},
ObjectMeta: metav1.ObjectMeta{
Name: "valid-status-template",
Namespace: "default",
},
Spec: EventTriggeredJobSpec{
StatusSelector: &StatusSelector{
ResourceKind: "Pod",
NamePattern: "test-*",
Conditions: []StatusCondition{
{
Type: "Ready",
Status: "True",
},
},
},
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test",
Image: "busybox",
Command: []string{"echo", "test"},
},
},
RestartPolicy: corev1.RestartPolicyNever,
},
},
},
},
},
}

// Validate required fields for EventSelector
if validEventTemplate.Spec.EventSelector != nil {
if validEventTemplate.Spec.EventSelector.ResourceKind == "" {
t.Errorf("ResourceKind is required but allowed to be empty in EventSelector")
}

if len(validEventTemplate.Spec.EventSelector.EventTypes) == 0 {
t.Errorf("EventTypes is required but allowed to be empty in EventSelector")
}
}

// Validate required fields for StatusSelector
if validStatusTemplate.Spec.StatusSelector != nil {
if validStatusTemplate.Spec.StatusSelector.ResourceKind == "" {
t.Errorf("ResourceKind is required but allowed to be empty in StatusSelector")
}

if len(validStatusTemplate.Spec.StatusSelector.Conditions) == 0 {
t.Errorf("At least one condition is required in StatusSelector")
}
}

// Verify at least one selector is provided
invalidTemplate := &EventTriggeredJob{
Spec: EventTriggeredJobSpec{
EventSelector: nil,
StatusSelector: nil,
},
}

if len(validTemplate.Spec.EventSelector.EventTypes) == 0 {
t.Errorf("EventTypes is required but allowed to be empty")
if invalidTemplate.Spec.EventSelector == nil && invalidTemplate.Spec.StatusSelector == nil {
// This is expected to fail in actual validation
t.Logf("Correctly identified that at least one selector is required")
}

// Read the CRD file
Expand Down
51 changes: 50 additions & 1 deletion pkg/apis/kubevent/v1alpha1/deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,16 @@ func (in *EventTriggeredJobList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out.
func (in *EventTriggeredJobSpec) DeepCopyInto(out *EventTriggeredJobSpec) {
*out = *in
in.EventSelector.DeepCopyInto(&out.EventSelector)
if in.EventSelector != nil {
in, out := &in.EventSelector, &out.EventSelector
*out = new(EventSelector)
(*in).DeepCopyInto(*out)
}
if in.StatusSelector != nil {
in, out := &in.StatusSelector, &out.StatusSelector
*out = new(StatusSelector)
(*in).DeepCopyInto(*out)
}
in.JobTemplate.DeepCopyInto(&out.JobTemplate)
}

Expand Down Expand Up @@ -131,3 +140,43 @@ func (in *EventTriggeredJobStatus) DeepCopy() *EventTriggeredJobStatus {
in.DeepCopyInto(out)
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out.
func (in *StatusCondition) DeepCopyInto(out *StatusCondition) {
*out = *in
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusCondition.
func (in *StatusCondition) DeepCopy() *StatusCondition {
if in == nil {
return nil
}
out := new(StatusCondition)
in.DeepCopyInto(out)
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out.
func (in *StatusSelector) DeepCopyInto(out *StatusSelector) {
*out = *in
if in.LabelSelector != nil {
in, out := &in.LabelSelector, &out.LabelSelector
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]StatusCondition, len(*in))
copy(*out, *in)
}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusSelector.
func (in *StatusSelector) DeepCopy() *StatusSelector {
if in == nil {
return nil
}
out := new(StatusSelector)
in.DeepCopyInto(out)
return out
}
65 changes: 62 additions & 3 deletions pkg/apis/kubevent/v1alpha1/deepcopy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func TestEventTriggeredJobDeepCopy(t *testing.T) {
Namespace: "default",
},
Spec: EventTriggeredJobSpec{
EventSelector: EventSelector{
EventSelector: &EventSelector{
ResourceKind: "Pod",
NamePattern: "test-*",
EventTypes: []string{"CREATE", "DELETE"},
Expand Down Expand Up @@ -111,7 +111,7 @@ func TestEventTriggeredJobListDeepCopy(t *testing.T) {
Namespace: "default",
},
Spec: EventTriggeredJobSpec{
EventSelector: EventSelector{
EventSelector: &EventSelector{
ResourceKind: "Pod",
},
},
Expand All @@ -122,7 +122,7 @@ func TestEventTriggeredJobListDeepCopy(t *testing.T) {
Namespace: "kube-system",
},
Spec: EventTriggeredJobSpec{
EventSelector: EventSelector{
EventSelector: &EventSelector{
ResourceKind: "Deployment",
},
},
Expand Down Expand Up @@ -177,3 +177,62 @@ func TestEventTriggeredJobListDeepCopy(t *testing.T) {
t.Errorf("Copy was affected by change to original Items length")
}
}

// Test StatusSelector DeepCopy
func TestStatusSelectorDeepCopy(t *testing.T) {
original := &StatusSelector{
ResourceKind: "Pod",
NamePattern: "test-*",
Conditions: []StatusCondition{
{
Type: "Ready",
Status: "True",
},
{
Type: "PodScheduled",
Status: "True",
Operator: "Equal",
},
},
}

// Test DeepCopy
copy := original.DeepCopy()

// Verify the copy is not the same object
if copy == original {
t.Errorf("DeepCopy returned the same object pointer")
}

// Verify all fields are copied correctly
if copy.ResourceKind != original.ResourceKind {
t.Errorf("Expected ResourceKind %s, got %s", original.ResourceKind, copy.ResourceKind)
}
if copy.NamePattern != original.NamePattern {
t.Errorf("Expected NamePattern %s, got %s", original.NamePattern, copy.NamePattern)
}
if len(copy.Conditions) != len(original.Conditions) {
t.Errorf("Expected Conditions length %d, got %d", len(original.Conditions), len(copy.Conditions))
}
if copy.Conditions[0].Type != original.Conditions[0].Type {
t.Errorf("Expected Condition Type %s, got %s", original.Conditions[0].Type, copy.Conditions[0].Type)
}
if copy.Conditions[0].Status != original.Conditions[0].Status {
t.Errorf("Expected Condition Status %s, got %s", original.Conditions[0].Status, copy.Conditions[0].Status)
}

// Now change something in the original and make sure the copy is not affected
original.ResourceKind = "Deployment"
original.Conditions[0].Status = "False"
original.Conditions = append(original.Conditions, StatusCondition{Type: "Available", Status: "True"})

if copy.ResourceKind == original.ResourceKind {
t.Errorf("Copy was affected by change to original ResourceKind")
}
if copy.Conditions[0].Status == original.Conditions[0].Status {
t.Errorf("Copy was affected by change to original Condition Status")
}
if len(copy.Conditions) == len(original.Conditions) {
t.Errorf("Copy was affected by change to original Conditions length")
}
}
Loading

0 comments on commit a2cab39

Please # to comment.