Skip to content

Commit 5659056

Browse files
author
Zeusro
committed
Mon Jan 12 07:20:25 CST 2026
1 parent 5e1645c commit 5659056

8 files changed

Lines changed: 420 additions & 50 deletions

File tree

cmd/killer/deployment_killer.go

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
package killer
22

33
import (
4-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4+
"context"
5+
"fmt"
6+
"math/rand"
7+
"time"
58

69
"github.com/p-program/kube-killer/core"
10+
"github.com/rs/zerolog/log"
11+
appsv1 "k8s.io/api/apps/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
713
"k8s.io/client-go/kubernetes"
14+
"k8s.io/client-go/util/retry"
815
)
916

10-
func init() {
11-
12-
}
13-
14-
// var newDeployKillerCommand = &cobra.Command{
15-
// Use: "deploy",
16-
// Short: "Kill kubernetes's deploy",
17-
// Long: ``,
18-
// Run: func(cmd *cobra.Command, args []string) {
19-
// fmt.Print("")
20-
// }}
21-
2217
type DeploymentKiller struct {
2318
client *kubernetes.Clientset
2419
deleteOption metav1.DeleteOptions
2520
dryRun bool
2621
mafia bool
22+
half bool
2723
namespace string
2824
}
2925

@@ -54,6 +50,107 @@ func (k *DeploymentKiller) DryRun() *DeploymentKiller {
5450
return k
5551
}
5652

57-
func (k *DeploymentKiller) Kill() {
53+
func (k *DeploymentKiller) SetHalf() *DeploymentKiller {
54+
k.half = true
55+
return k
56+
}
57+
58+
// Kill deletes deployments based on the mode
59+
// - If mafia is true and half is true: deletes half of the deployments randomly
60+
// - If mafia is true: deletes all deployments
61+
// - Otherwise: deletes all deployments (default behavior, similar to kubectl delete deploy)
62+
func (k *DeploymentKiller) Kill() error {
63+
if k.mafia {
64+
if k.half {
65+
return k.KillHalfDeployments()
66+
}
67+
return k.KillAllDeployments()
68+
}
69+
return k.KillAllDeployments()
70+
}
71+
72+
// KillAllDeployments deletes all deployments in the namespace
73+
func (k *DeploymentKiller) KillAllDeployments() error {
74+
deployments, err := k.client.AppsV1().Deployments(k.namespace).List(context.TODO(), metav1.ListOptions{})
75+
if err != nil {
76+
return fmt.Errorf("failed to list deployments: %w", err)
77+
}
78+
79+
if len(deployments.Items) == 0 {
80+
log.Info().Msgf("No deployments found in namespace %s", k.namespace)
81+
return nil
82+
}
83+
84+
log.Info().Msgf("Found %d deployment(s) in namespace %s", len(deployments.Items), k.namespace)
85+
86+
for _, deploy := range deployments.Items {
87+
if err := k.deleteDeployment(deploy); err != nil {
88+
log.Error().Err(err).Msgf("Failed to delete deployment %s", deploy.Name)
89+
// Continue with other deployments
90+
}
91+
}
92+
93+
return nil
94+
}
95+
96+
// KillHalfDeployments deletes half of the deployments randomly
97+
func (k *DeploymentKiller) KillHalfDeployments() error {
98+
deployments, err := k.client.AppsV1().Deployments(k.namespace).List(context.TODO(), metav1.ListOptions{})
99+
if err != nil {
100+
return fmt.Errorf("failed to list deployments: %w", err)
101+
}
102+
103+
if len(deployments.Items) == 0 {
104+
log.Info().Msgf("No deployments found in namespace %s", k.namespace)
105+
return nil
106+
}
107+
108+
// Randomly shuffle the deployments list
109+
deployList := deployments.Items
110+
r := rand.New(rand.NewSource(time.Now().UnixNano()))
111+
r.Shuffle(len(deployList), func(i, j int) {
112+
deployList[i], deployList[j] = deployList[j], deployList[i]
113+
})
114+
115+
// Calculate how many deployments to kill (half, rounded down)
116+
deploysToKill := len(deployList) / 2
117+
if deploysToKill == 0 {
118+
deploysToKill = 1 // At least kill one deployment if there's only one
119+
}
120+
121+
log.Info().Msgf("Killing %d out of %d deployment(s) in namespace %s", deploysToKill, len(deployList), k.namespace)
122+
123+
for i := 0; i < deploysToKill; i++ {
124+
deploy := deployList[i]
125+
if err := k.deleteDeployment(deploy); err != nil {
126+
log.Error().Err(err).Msgf("Failed to delete deployment %s", deploy.Name)
127+
// Continue with other deployments
128+
}
129+
}
130+
131+
return nil
132+
}
133+
134+
// deleteDeployment deletes a single deployment
135+
func (k *DeploymentKiller) deleteDeployment(deploy appsv1.Deployment) error {
136+
deployName := deploy.Name
137+
138+
if k.dryRun {
139+
log.Info().Msgf("[DRY RUN] Would delete deployment %s in namespace %s", deployName, k.namespace)
140+
return nil
141+
}
142+
143+
log.Warn().Msgf("Deleting deployment %s in namespace %s", deployName, k.namespace)
144+
145+
// Use retry for deletion in case of conflicts
146+
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
147+
return k.client.AppsV1().Deployments(k.namespace).Delete(context.TODO(), deployName, k.deleteOption)
148+
})
149+
150+
if err != nil {
151+
return fmt.Errorf("failed to delete deployment %s: %w", deployName, err)
152+
}
58153

154+
log.Info().Msgf("Successfully deleted deployment %s in namespace %s", deployName, k.namespace)
155+
return nil
59156
}

cmd/killer/job_builder.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package killer
22

33
import (
4-
"time"
5-
64
batchv1 "k8s.io/api/batch/v1"
75
v1 "k8s.io/api/core/v1"
86
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -37,16 +35,6 @@ func newJobBuilder() *JobBuilder {
3735
return &b
3836
}
3937

40-
func (b *JobBuilder) AddNamespace(ns string) *JobBuilder {
41-
b.job.ObjectMeta.Namespace = ns
42-
return b
43-
}
44-
45-
func (b *JobBuilder) RunAt(date time.Time) *JobBuilder {
46-
47-
return b
48-
}
49-
5038
func (b *JobBuilder) Build() *batchv1.Job {
5139
return b.job
5240
}

cmd/killer/kill.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,13 @@ func SelectKiller(args []string) error {
126126
if dryRun {
127127
k.DryRun()
128128
}
129-
k.Kill()
130-
return nil
129+
if mafia {
130+
k.BlackHand()
131+
if half {
132+
k.SetHalf()
133+
}
134+
}
135+
return k.Kill()
131136
})
132137
}
133138
k, err := NewDeploymentKiller(namespace)
@@ -137,8 +142,13 @@ func SelectKiller(args []string) error {
137142
if dryRun {
138143
k.DryRun()
139144
}
140-
k.Kill()
141-
return nil
145+
if mafia {
146+
k.BlackHand()
147+
if half {
148+
k.SetHalf()
149+
}
150+
}
151+
return k.Kill()
142152
case "pv":
143153
// PV is cluster-scoped, no need for namespace iteration
144154
k, err := NewPVKiller()

cmd/server/api/v1alpha1/kubekiller_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ type KubeKillerSpec struct {
1515
// +kubebuilder:default="5m"
1616
Interval string `json:"interval,omitempty"`
1717

18+
// ScheduleAt defines a specific time to execute the deletion task (RFC3339 format)
19+
// If set, interval will be ignored and task will run only once at the specified time
20+
// +optional
21+
ScheduleAt *metav1.Time `json:"scheduleAt,omitempty"`
22+
1823
// Namespaces to operate on. Empty means all namespaces except kube-system
1924
// +optional
2025
Namespaces []string `json:"namespaces,omitempty"`

cmd/server/controllers/kubekiller_controller.go

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,28 +42,55 @@ func (r *KubeKillerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
4242
return ctrl.Result{}, client.IgnoreNotFound(err)
4343
}
4444

45-
// Parse interval
46-
interval, err := time.ParseDuration(kk.Spec.Interval)
47-
if err != nil {
48-
// Default to 5 minutes if parsing fails
49-
interval = 5 * time.Minute
50-
log.Warn().Err(err).Msgf("Failed to parse interval, using default 5m")
51-
}
45+
// Check if scheduleAt is set (specific time point execution)
46+
if kk.Spec.ScheduleAt != nil {
47+
now := time.Now()
48+
scheduleTime := kk.Spec.ScheduleAt.Time
49+
50+
// Check if task has already been executed
51+
if kk.Status.LastRunTime != nil {
52+
lastRunTime := kk.Status.LastRunTime.Time
53+
// If last run time is at or after the scheduled time, task has been executed
54+
if !lastRunTime.Before(scheduleTime) {
55+
log.Info().Msgf("Scheduled task at %s has already been executed at %s, skipping",
56+
scheduleTime.Format(time.RFC3339), lastRunTime.Format(time.RFC3339))
57+
return ctrl.Result{}, nil
58+
}
59+
}
5260

53-
// Check if we should run now
54-
shouldRun := true
55-
if kk.Status.LastRunTime != nil {
56-
timeSinceLastRun := time.Since(kk.Status.LastRunTime.Time)
57-
if timeSinceLastRun < interval {
58-
shouldRun = false
59-
// Requeue after the remaining time
60-
requeueAfter := interval - timeSinceLastRun
61+
// If the scheduled time hasn't arrived yet, requeue
62+
if now.Before(scheduleTime) {
63+
requeueAfter := scheduleTime.Sub(now)
64+
log.Info().Msgf("Scheduled task will run at %s, requeuing in %v", scheduleTime.Format(time.RFC3339), requeueAfter)
6165
return ctrl.Result{RequeueAfter: requeueAfter}, nil
6266
}
63-
}
6467

65-
if !shouldRun {
66-
return ctrl.Result{RequeueAfter: interval}, nil
68+
// Scheduled time has arrived, execute the task
69+
log.Info().Msgf("Scheduled time %s has arrived, executing task", scheduleTime.Format(time.RFC3339))
70+
} else {
71+
// Use interval-based execution (original behavior)
72+
interval, err := time.ParseDuration(kk.Spec.Interval)
73+
if err != nil {
74+
// Default to 5 minutes if parsing fails
75+
interval = 5 * time.Minute
76+
log.Warn().Err(err).Msgf("Failed to parse interval, using default 5m")
77+
}
78+
79+
// Check if we should run now
80+
shouldRun := true
81+
if kk.Status.LastRunTime != nil {
82+
timeSinceLastRun := time.Since(kk.Status.LastRunTime.Time)
83+
if timeSinceLastRun < interval {
84+
shouldRun = false
85+
// Requeue after the remaining time
86+
requeueAfter := interval - timeSinceLastRun
87+
return ctrl.Result{RequeueAfter: requeueAfter}, nil
88+
}
89+
}
90+
91+
if !shouldRun {
92+
return ctrl.Result{RequeueAfter: interval}, nil
93+
}
6794
}
6895

6996
// Update status to Running
@@ -100,7 +127,17 @@ func (r *KubeKillerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
100127
return ctrl.Result{}, err
101128
}
102129

103-
// Requeue after interval
130+
// If scheduleAt was set and task has been executed, don't requeue
131+
if kk.Spec.ScheduleAt != nil {
132+
log.Info().Msg("Scheduled task completed, not requeuing")
133+
return ctrl.Result{}, err2
134+
}
135+
136+
// Requeue after interval for interval-based execution
137+
interval, _ := time.ParseDuration(kk.Spec.Interval)
138+
if interval == 0 {
139+
interval = 5 * time.Minute
140+
}
104141
return ctrl.Result{RequeueAfter: interval}, err2
105142
}
106143

deploy/operator/crd.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ spec:
2525
interval:
2626
type: string
2727
default: "5m"
28-
description: How often the killer should run (e.g., "5m", "1h")
28+
description: How often the killer should run (e.g., "5m", "1h"). Ignored if scheduleAt is set.
29+
scheduleAt:
30+
type: string
31+
format: date-time
32+
description: Specific time to execute the deletion task (RFC3339 format, e.g., "2026-01-15T10:30:00Z"). If set, interval will be ignored and task will run only once at the specified time.
2933
namespaces:
3034
type: array
3135
items:

deploy/operator/example.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,58 @@ spec:
2828
- kube-system
2929
- kube-public
3030
- kube-node-lease
31+
---
32+
# Example: Specific namespace deletion
33+
apiVersion: kubekiller.p-program.github.io/v1alpha1
34+
kind: KubeKiller
35+
metadata:
36+
name: kube-killer-specific-namespace
37+
namespace: default
38+
spec:
39+
mode: illidan
40+
interval: "15m"
41+
dryRun: false
42+
# Only operate on specific namespaces
43+
namespaces:
44+
- production
45+
- staging
46+
- development
47+
resources:
48+
- pod
49+
- job
50+
- configmap
51+
- secret
52+
---
53+
# Example: Scheduled execution at specific time point
54+
apiVersion: kubekiller.p-program.github.io/v1alpha1
55+
kind: KubeKiller
56+
metadata:
57+
name: kube-killer-scheduled
58+
namespace: default
59+
spec:
60+
mode: illidan
61+
# scheduleAt takes precedence over interval
62+
# Task will execute only once at the specified time (RFC3339 format)
63+
scheduleAt: "2026-01-15T10:30:00Z"
64+
dryRun: false
65+
namespaces:
66+
- production
67+
resources:
68+
- pod
69+
- job
70+
---
71+
# Example: Scheduled execution with specific namespaces
72+
apiVersion: kubekiller.p-program.github.io/v1alpha1
73+
kind: KubeKiller
74+
metadata:
75+
name: kube-killer-scheduled-namespace
76+
namespace: default
77+
spec:
78+
mode: demon
79+
# Execute at specific time point in specific namespaces
80+
scheduleAt: "2026-01-20T02:00:00Z"
81+
dryRun: false
82+
namespaces:
83+
- test
84+
- dev
3185

0 commit comments

Comments
 (0)