├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ ├── release.yml
│ ├── lint-pr.yml
│ └── homebrew.yml
├── .gitignore
├── example
└── sample-config.json
├── formula
└── chaossnake.rb
├── .goreleaser.yml
├── backend
├── go.mod
├── cmd
│ └── main.go
├── go.sum
└── pkg
│ └── k8s
│ └── kubernetes.go
└── README.md
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @deggja
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | chaossnake
2 | chaos.log
--------------------------------------------------------------------------------
/example/sample-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "resource_types": ["pods", "replicasets", "deployments"],
3 | "namespaces": {
4 | "include": ["grafana", "default", "netfetch", "podinfo", "workloads"],
5 | "exclude": ["kube-system"]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | rebase-strategy: disabled
5 | directory: "/"
6 | schedule:
7 | interval: "daily"
8 | groups:
9 | actions:
10 | patterns:
11 | - '*'
12 | - package-ecosystem: "gomod"
13 | rebase-strategy: "disabled"
14 | directory: "/backend"
15 | schedule:
16 | interval: "daily"
17 | groups:
18 | go:
19 | patterns:
20 | - '*'
21 |
--------------------------------------------------------------------------------
/formula/chaossnake.rb:
--------------------------------------------------------------------------------
1 | class Chaossnake < Formula
2 | desc "Play snake in your terminal and wreck havoc to your Kubernetes cluster. Lol."
3 | homepage "https://github.com/deggja/chaossnake"
4 |
5 | if OS.mac?
6 | url "https://github.com/deggja/chaossnake/releases/download/v0.3.0/chaossnake_0.3.0_darwin_amd64.tar.gz"
7 | sha256 "2c70292ff95632d54e0c6a8e14d8b5ec53d548b63eb90b23161ded1cf3f0d30d"
8 | elsif OS.linux?
9 | url "https://github.com/deggja/chaossnake/releases/download/v0.3.0/chaossnake_0.3.0_linux_amd64.tar.gz"
10 | sha256 "5f9f13bb9dd2fa2ff5627f3fbed87cf5945a8d6734e065da432acb3c6f625f78"
11 | end
12 |
13 | def install
14 | bin.install "chaossnake"
15 | end
16 |
17 | test do
18 | system "#{bin}/chaossnake", "version"
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 |
8 | jobs:
9 | build-and-release:
10 | name: Build and Release
11 | runs-on: ubuntu-20.04
12 | steps:
13 | - name: Check out code
14 | uses: actions/checkout@v6
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Set up Go
19 | uses: actions/setup-go@v6
20 | with:
21 | go-version: '1.21'
22 |
23 | - name: Run Go Mod Tidy
24 | run: |
25 | cd backend
26 | go mod tidy
27 |
28 | - name: Run GoReleaser
29 | uses: goreleaser/goreleaser-action@v6
30 | with:
31 | version: '~> v2'
32 | args: release --clean
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | # .goreleaser.yml
3 | project_name: chaossnake
4 |
5 | # Changelog Configuration
6 | changelog:
7 | sort: desc
8 | filters:
9 | exclude:
10 | - '^Merge pull request'
11 | groups:
12 | - title: Features
13 | regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
14 | order: 0
15 | - title: "Bug fixes"
16 | regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$'
17 | order: 1
18 | - title: "Documentation Updates"
19 | regexp: '^.*?docs(\([[:word:]]+\))??!?:.+$'
20 | order: 2
21 | - title: "Other Changes"
22 | regexp: "^(ci|build|misc|perf|deps):"
23 | order: 3
24 | - title: "Miscellaneous"
25 | regexp: ".*"
26 | order: 4
27 |
28 | builds:
29 | - id: "chaossnake"
30 | main: ./cmd/main.go
31 | dir: ./backend
32 | binary: chaossnake
33 | goos:
34 | - linux
35 | - darwin
36 | - windows
37 | goarch:
38 | - amd64
39 | - arm64
40 | ignore:
41 | - goos: darwin
42 | goarch: arm64
43 |
44 | archives:
45 | - id: "archive"
46 | builds:
47 | - chaossnake
48 | format: tar.gz
49 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
50 | wrap_in_directory: true
51 |
52 | # Release configuration
53 | release:
54 | github:
55 | owner: deggja
56 | name: chaossnake
57 | draft: false
58 |
--------------------------------------------------------------------------------
/.github/workflows/lint-pr.yml:
--------------------------------------------------------------------------------
1 | name: "Lint PR"
2 |
3 | on:
4 | pull_request_target:
5 | types:
6 | - opened
7 | - edited
8 | - synchronize
9 |
10 | jobs:
11 | main:
12 | name: Validate PR title
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: lint_pr_title
16 | id: lint_pr_title
17 | uses: amannn/action-semantic-pull-request@v6.1.1
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 | - uses: marocchino/sticky-pull-request-comment@v2
21 | if: always() && (steps.lint_pr_title.outputs.error_message != null)
22 | with:
23 | header: pr-title-lint-error
24 | message: |
25 | Hey there and thank you for opening this pull request! 👋🏼
26 |
27 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
28 |
29 | Details:
30 |
31 | ```
32 | ${{ steps.lint_pr_title.outputs.error_message }}
33 | ```
34 | # Delete a previous comment when the issue has been resolved
35 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }}
36 | uses: marocchino/sticky-pull-request-comment@v2
37 | with:
38 | header: pr-title-lint-error
39 | delete: true
--------------------------------------------------------------------------------
/backend/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/deggja/chaossnake
2 |
3 | go 1.24.0
4 |
5 | toolchain go1.24.1
6 |
7 | require (
8 | github.com/JoelOtter/termloop v0.0.0-20210806173944-5f7c38744afb
9 | k8s.io/api v0.34.2
10 | k8s.io/apimachinery v0.34.2
11 | k8s.io/client-go v0.34.2
12 | )
13 |
14 | require (
15 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
16 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect
17 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect
18 | github.com/go-logr/logr v1.4.2 // indirect
19 | github.com/go-openapi/jsonpointer v0.21.0 // indirect
20 | github.com/go-openapi/jsonreference v0.20.2 // indirect
21 | github.com/go-openapi/swag v0.23.0 // indirect
22 | github.com/gogo/protobuf v1.3.2 // indirect
23 | github.com/google/gnostic-models v0.7.0 // indirect
24 | github.com/google/uuid v1.6.0 // indirect
25 | github.com/josharian/intern v1.0.0 // indirect
26 | github.com/json-iterator/go v1.1.12 // indirect
27 | github.com/mailru/easyjson v0.7.7 // indirect
28 | github.com/mattn/go-runewidth v0.0.9 // indirect
29 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
30 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
31 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
32 | github.com/nsf/termbox-go v1.1.1 // indirect
33 | github.com/pkg/errors v0.9.1 // indirect
34 | github.com/spf13/pflag v1.0.6 // indirect
35 | github.com/x448/float16 v0.8.4 // indirect
36 | go.yaml.in/yaml/v2 v2.4.2 // indirect
37 | go.yaml.in/yaml/v3 v3.0.4 // indirect
38 | golang.org/x/net v0.38.0 // indirect
39 | golang.org/x/oauth2 v0.27.0 // indirect
40 | golang.org/x/sys v0.31.0 // indirect
41 | golang.org/x/term v0.30.0 // indirect
42 | golang.org/x/text v0.23.0 // indirect
43 | golang.org/x/time v0.9.0 // indirect
44 | google.golang.org/protobuf v1.36.5 // indirect
45 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
46 | gopkg.in/inf.v0 v0.9.1 // indirect
47 | gopkg.in/yaml.v3 v3.0.1 // indirect
48 | k8s.io/klog/v2 v2.130.1 // indirect
49 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
50 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
51 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
52 | sigs.k8s.io/randfill v1.0.0 // indirect
53 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
54 | sigs.k8s.io/yaml v1.6.0 // indirect
55 | )
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
Chaos Snake
10 | Control the snake. Eat resources. Create chaos.
11 |
12 |
13 |
14 | ## Contents
15 | - [**What is Chaos Snake?**](#-what-is-chaos-snake-)
16 | - **[Installation](#installation)**
17 | - [Build from source](#build-from-source-)
18 | - [Precompiled binaries](#precompiled-binaries-)
19 | - [**Usage**](#usage)
20 | - [Starting the game](#starting-the-game-)
21 | - [Playing Chaos Snake](#playing-chaos-snake-)
22 | - [Kubernetes interaction](#kubernetes-interaction-)
23 | - [**Contribute**](#contribute-)
24 | - [**Acknowledgements**](#acknowledgments)
25 |
26 | ## ⭐ What is Chaos Snake? ⭐
27 |
28 | Chaos Snake lets you play snake while wrecking havock in your Kubernetes cluster. Have fun while you can.
29 |
30 | ### How does it work? 🤔
31 |
32 | Each piece of food you eat corresponds to a pod in your cluster (I left out kube-system though..).
33 |
34 | ## Installation
35 |
36 | ### Homebrew 🍺
37 |
38 | To install Chaos Snake using Homebrew, you can run the following commands:
39 |
40 | ```sh
41 | brew tap deggja/chaossnake https://github.com/deggja/chaossnake
42 | brew install chaossnake
43 | ```
44 |
45 | ### Build from source 💻
46 |
47 | To build Chaos Snake from the source, you need a working Go environment with version 1.21 or higher. Follow these steps:
48 |
49 | ```sh
50 | git clone https://github.com/deggja/chaossnake.git --depth 1
51 | cd chaossnake
52 | go build -o chaossnake
53 | ```
54 |
55 | ## Usage
56 |
57 | ### Starting the game
58 |
59 | To start the game, simply run the compiled binary:
60 |
61 | ```sh
62 | ./chaossnake
63 | ```
64 |
65 | This will run the game in `default mode`. The snake will only eat resources of type `pod` and avoid system critical workloads in `kube-system`.
66 |
67 | To specify a configuration file, use the `--config/-c` flag.
68 |
69 | ```sh
70 | ./chaossnake --config config.json
71 | ```
72 | This will run the game in `config mode`. The snake will eat all resource types in all namespaces defined in the configuration file.
73 |
74 | ### Example Configuration File
75 |
76 | ```json
77 | {
78 | "resource_types": ["pods", "replicasets", "deployments", "services"],
79 | "namespaces": {
80 | "include": ["grafana", "default", "netfetch", "podinfo", "workloads"],
81 | "exclude": ["kube-system"]
82 | }
83 | ```
84 |
85 | ## Playing Chaos Snake
86 |
87 | Use the arrow keys to navigate the snake around the screen:
88 |
89 | | Key | Action |
90 | |-----------------|----------------------|
91 | | Arrow up | Move up |
92 | | Arrow down | Move down |
93 | | Arrow left | Move left |
94 | | Arrow right | Move right |
95 | | Space | Pause or Resume |
96 | | CTRL + C | Quit the game |
97 |
98 | [](https://asciinema.org/a/Q4usmR4HB8LhHojJA9qJeQmdX)
99 |
100 | ## Kubernetes interaction
101 |
102 | Chaos Snake will needs access to a Kubernetes cluster. Ensure your `kubeconfig` is set up correctly before starting the game. The application currently expects the default kubeconfig or a kubeconfig environment variable.
103 |
104 | As you play and the pods are deleted, Chaos Snake will log its actions to a `chaos.log` file for your review.
105 |
106 | ## Contribute 🔨
107 |
108 | Feel free to dive in! [Open an issue](https://github.com/deggja/chaossnake/issues) or submit PRs.
109 |
110 | ## Acknowledgments
111 |
112 | This project utilizes [Termloop](https://github.com/JoelOtter/termloop), a simple Go library for creating terminal-based games. Thanks to the creators and contributors of Termloop for providing such a versatile tool.
113 |
114 | Chaos Snake is inspired by [Chaos Monkey](https://github.com/Netflix/chaosmonkey) from Netflix as well as [Kubeinvaders](https://github.com/lucky-sideburn/kubeinvaders) - so thank you to these awesome projects.
115 |
116 | ## License
117 |
118 | Chaos Snake is released under the MIT License. Check out the [LICENSE](https://github.com/deggja/chaossnake/LICENSE) file for more information.
119 |
--------------------------------------------------------------------------------
/.github/workflows/homebrew.yml:
--------------------------------------------------------------------------------
1 | name: Update Homebrew Formula
2 |
3 | on:
4 | release:
5 | types: [published]
6 | workflow_dispatch:
7 | inputs:
8 | tag:
9 | description: 'Release tag to update Homebrew formula (e.g., v1.2.3)'
10 | required: true
11 | default: ''
12 |
13 | jobs:
14 | update-homebrew:
15 | runs-on: ubuntu-latest
16 |
17 | permissions:
18 | contents: write
19 | pull-requests: write
20 |
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v6
24 | with:
25 | fetch-depth: 0
26 |
27 | - name: Extract release version
28 | id: extract_version
29 | run: |
30 | if [ "${{ github.event_name }}" == "release" ]; then
31 | TAG="${{ github.event.release.tag_name }}"
32 | VERSION="${TAG#v}"
33 | echo "Using release tag: $TAG"
34 | elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
35 | TAG="${{ github.event.inputs.tag }}"
36 | VERSION="${TAG#v}"
37 | echo "Using workflow_dispatch tag: $TAG"
38 | else
39 | echo "Unsupported event: $GITHUB_EVENT_NAME"
40 | exit 1
41 | fi
42 | echo "VERSION=$VERSION" >> $GITHUB_ENV
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 |
46 | - name: Create temporary directory
47 | run: mkdir temp
48 |
49 | - name: Download macOS tarball
50 | run: |
51 | wget https://github.com/deggja/chaossnake/releases/download/v${{ env.VERSION }}/chaossnake_${{ env.VERSION }}_darwin_amd64.tar.gz -O temp/chaossnake_darwin_amd64.tar.gz
52 |
53 | - name: Calculate macOS SHA256
54 | run: |
55 | SHA=$(sha256sum temp/chaossnake_darwin_amd64.tar.gz | awk '{print $1}')
56 | echo "MACOS_SHA256=$SHA" >> $GITHUB_ENV
57 |
58 | - name: Download Linux tarball
59 | run: |
60 | wget https://github.com/deggja/chaossnake/releases/download/v${{ env.VERSION }}/chaossnake_${{ env.VERSION }}_linux_amd64.tar.gz -O temp/chaossnake_linux_amd64.tar.gz
61 |
62 | - name: Calculate Linux SHA256
63 | run: |
64 | SHA=$(sha256sum temp/chaossnake_linux_amd64.tar.gz | awk '{print $1}')
65 | echo "LINUX_SHA256=$SHA" >> $GITHUB_ENV
66 |
67 | - name: Update Homebrew Formula
68 | run: |
69 | FORMULA_FILE="formula/chaossnake.rb"
70 | sed -i "s|url \".*darwin_amd64\.tar\.gz\"|url \"https://github.com/deggja/chaossnake/releases/download/v${{ env.VERSION }}/chaossnake_${{ env.VERSION }}_darwin_amd64.tar.gz\"|" $FORMULA_FILE
71 | sed -i -E "/if OS\.mac\?/,/elsif OS\.linux\?/ s|sha256 \".*\"|sha256 \"${{ env.MACOS_SHA256 }}\"|" $FORMULA_FILE
72 | sed -i "s|url \".*linux_amd64\.tar\.gz\"|url \"https://github.com/deggja/chaossnake/releases/download/v${{ env.VERSION }}/chaossnake_${{ env.VERSION }}_linux_amd64.tar.gz\"|" $FORMULA_FILE
73 | sed -i -E "/elsif OS\.linux\?/,/end/ s|sha256 \".*\"|sha256 \"${{ env.LINUX_SHA256 }}\"|" $FORMULA_FILE
74 |
75 | - name: Fetch Latest Changes from Main
76 | run: |
77 | git fetch origin main
78 | git checkout main
79 | git pull origin main
80 |
81 | - name: Remove temp directory
82 | run: rm -rf temp
83 |
84 | - name: Commit and Push changes
85 | uses: stefanzweifel/git-auto-commit-action@v7
86 | with:
87 | commit_message: "chore: update homebrew formula for v${{ env.VERSION }}"
88 | branch: chore/homebrew-${{ env.VERSION }}
89 | create_branch: true
90 | file_pattern: "formula/chaossnake.rb"
91 | commit_user_name: "chaossnake Bot"
92 | commit_author: "chaossnake Bot "
93 | commit_user_email: "bot@chaossnake.com"
94 |
95 | - name: Create Pull Request Using GitHub CLI
96 | env:
97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
98 | run: |
99 | gh pr create \
100 | --title "chore: update homebrew formula for v${{ env.VERSION }}" \
101 | --body "This is an automated pull request. This pull request updates the Homebrew formula to version v${{ env.VERSION }} with the latest binary URLs and checksums." \
102 | --base main \
103 | --head chore/homebrew-${{ env.VERSION }} \
104 | --label homebrew,automated \
105 | --reviewer deggja \
106 |
--------------------------------------------------------------------------------
/backend/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "math/rand"
8 | "os"
9 | "time"
10 |
11 | tl "github.com/JoelOtter/termloop"
12 | "github.com/deggja/chaossnake/pkg/k8s"
13 | )
14 |
15 | type Coordinates struct {
16 | X, Y int
17 | }
18 |
19 | type Snake struct {
20 | body []Coordinates
21 | direction string
22 | tickCount int
23 | growth int
24 | }
25 |
26 | type Food struct {
27 | *tl.Entity
28 | placed bool
29 | }
30 |
31 | var foodPodMappings []FoodPodMapping
32 | var deletedPodTextEntities []*tl.Text
33 |
34 | type FoodPodMapping struct {
35 | foodEntity *Food
36 | resourceInfo k8s.ResourceInfo
37 | }
38 |
39 | const (
40 | LevelWidth = 80
41 | LevelHeight = 24
42 | )
43 |
44 | func NewFood() *Food {
45 | return &Food{
46 | Entity: tl.NewEntityFromCanvas(2, 2, tl.CanvasFromString("O")),
47 | placed: false,
48 | }
49 | }
50 |
51 | func (f *Food) Tick(event tl.Event) {
52 | // Check if food has been placed, if not, place the food
53 | if !f.placed {
54 | width, height := game.Screen().Size()
55 | if width > 0 && height > 0 {
56 | f.PlaceFood(width, height)
57 | f.placed = true
58 | }
59 | }
60 | }
61 |
62 | func (f *Food) PlaceFood(levelWidth, levelHeight int) {
63 | rand.Seed(time.Now().UnixNano())
64 | foodX := rand.Intn(LevelWidth-4) + 2
65 | foodY := rand.Intn(LevelHeight-4) + 2
66 |
67 | f.SetPosition(foodX, foodY)
68 |
69 | // Get a random resource name and namespace to associate with this food
70 | select {
71 | case resourceInfo := <-k8s.ResourceInfoQueue:
72 | foodPodMappings = append(foodPodMappings, FoodPodMapping{
73 | foodEntity: f,
74 | resourceInfo: resourceInfo,
75 | })
76 | default:
77 | log.Println("No resource info available at the moment.")
78 | }
79 | }
80 |
81 | func (f *Food) Draw(screen *tl.Screen) {
82 | // Draw food after it has been placed
83 | if f.placed {
84 | f.Entity.Draw(screen)
85 | }
86 | }
87 |
88 | func (f *Food) AtPosition(x, y int) bool {
89 | foodX, foodY := f.Position()
90 | // Check for collision in a wider range for X to accommodate faster horizontal movement
91 | return (x == foodX || x == foodX-1 || x == foodX+1) && y == foodY
92 | }
93 |
94 | func drawWalls(screen *tl.Screen) {
95 | // Top and bottom walls
96 | for x := 0; x < LevelWidth; x++ {
97 | screen.RenderCell(x, 0, &tl.Cell{Fg: tl.ColorWhite, Ch: '-'}) // Top wall
98 | screen.RenderCell(x, LevelHeight-1, &tl.Cell{Fg: tl.ColorWhite, Ch: '-'}) // Bottom wall
99 | }
100 | // Left and right walls
101 | for y := 0; y < LevelHeight; y++ {
102 | screen.RenderCell(0, y, &tl.Cell{Fg: tl.ColorWhite, Ch: '|'}) // Left wall
103 | screen.RenderCell(LevelWidth-1, y, &tl.Cell{Fg: tl.ColorWhite, Ch: '|'}) // Right wall
104 | }
105 | }
106 |
107 | func (snake *Snake) CollidesWithWalls() bool {
108 | head := snake.body[0]
109 | return head.X < 1 || head.Y < 1 || head.X >= LevelWidth-1 || head.Y >= LevelHeight-1
110 | }
111 |
112 | func (snake *Snake) CollidesWithSelf() bool {
113 | head := snake.body[0]
114 | for _, segment := range snake.body[1:] {
115 | if head.X == segment.X && head.Y == segment.Y {
116 | return true
117 | }
118 | }
119 | return false
120 | }
121 |
122 | func GameOver() {
123 | for _, entity := range deletedPodTextEntities {
124 | game.Screen().RemoveEntity(entity)
125 | }
126 | deletedPodTextEntities = nil
127 |
128 | showFinalScreen()
129 | log.Println("Game Over!")
130 | }
131 |
132 | func NewSnake(x, y int) *Snake {
133 | snake := &Snake{
134 | direction: "right",
135 | tickCount: 0,
136 | growth: 0,
137 | }
138 | // Initialize snake with 3 segments
139 | for i := 0; i < 3; i++ {
140 | snake.body = append(snake.body, Coordinates{X: x - i*2, Y: y})
141 | }
142 | return snake
143 | }
144 |
145 | func (snake *Snake) Draw(screen *tl.Screen) {
146 | drawWalls(screen)
147 | for _, segment := range snake.body {
148 | screen.RenderCell(segment.X, segment.Y, &tl.Cell{Fg: tl.ColorGreen, Ch: '■'})
149 | }
150 | }
151 |
152 | func showFinalScreen() {
153 | // Set up a blank level to display end game information
154 | blankLevel := tl.NewBaseLevel(tl.Cell{
155 | Bg: tl.ColorBlack, // Background color of the level
156 | Ch: ' ', // Character to fill the screen with
157 | })
158 | game.Screen().SetLevel(blankLevel)
159 |
160 | // Create the final score message
161 | finalMessage := fmt.Sprintf("Final Score: %d", score)
162 | messageLength := len(finalMessage)
163 | startX := (LevelWidth / 2) - (messageLength / 2)
164 | startY := LevelHeight / 2 - 1 // Positioned slightly above center for multiple lines
165 |
166 | finalScoreText := tl.NewText(startX, startY, finalMessage, tl.ColorWhite, tl.ColorBlack)
167 | blankLevel.AddEntity(finalScoreText)
168 |
169 | // Instructions for restarting or quitting
170 | restartMessage := "Press CTRL+C to QUIT"
171 | restartX := (LevelWidth / 2) - (len(restartMessage) / 2)
172 | restartY := startY + 2
173 |
174 | restartText := tl.NewText(restartX, restartY, restartMessage, tl.ColorWhite, tl.ColorBlack)
175 | blankLevel.AddEntity(restartText)
176 |
177 | game.Screen().Draw()
178 | }
179 |
180 | func updatePauseTextPosition() {
181 | message := "Game paused. Press space to resume or CTRL+C to quit."
182 | messageLength := len(message)
183 |
184 | // Center horizontally
185 | startX := (LevelWidth / 2) - (messageLength / 2)
186 | // Center vertically
187 | startY := LevelHeight / 2
188 |
189 | pauseText.SetPosition(startX, startY)
190 | }
191 |
192 | func DeletedResourceText(screen *tl.Screen, resourceType, resourceName, namespace string, startX, startY int) {
193 | for _, entity := range deletedPodTextEntities {
194 | screen.RemoveEntity(entity)
195 | }
196 | deletedPodTextEntities = nil
197 | lines := []struct {
198 | prefix string
199 | value string
200 | }{
201 | {"Resource: ", resourceType},
202 | {"Name: ", resourceName},
203 | {"Namespace: ", namespace},
204 | }
205 |
206 | generalMessage := "Oh no! Seems like the snake ate something.."
207 | generalMessageEntity := tl.NewText(startX, startY, generalMessage, tl.ColorWhite, tl.ColorBlack)
208 | screen.AddEntity(generalMessageEntity)
209 | deletedPodTextEntities = append(deletedPodTextEntities, generalMessageEntity)
210 |
211 | for i, line := range lines {
212 | prefixEntity := tl.NewText(startX, startY+1+i, line.prefix, tl.ColorWhite, tl.ColorBlack)
213 | valueEntity := tl.NewText(startX+len(line.prefix), startY+1+i, line.value, tl.ColorGreen, tl.ColorBlack)
214 |
215 | screen.AddEntity(prefixEntity)
216 | screen.AddEntity(valueEntity)
217 |
218 | deletedPodTextEntities = append(deletedPodTextEntities, prefixEntity, valueEntity)
219 | }
220 | }
221 |
222 | var score int
223 |
224 | func (snake *Snake) Tick(event tl.Event) {
225 | // Check for pause toggle first
226 | if event.Type == tl.EventKey && event.Key == tl.KeySpace {
227 | isPaused = !isPaused
228 | if isPaused {
229 | updatePauseTextPosition()
230 | } else {
231 | pauseText.SetPosition(-1, -1)
232 | }
233 | return
234 | }
235 |
236 | if isPaused {
237 | return
238 | }
239 |
240 | // Handle direction change input
241 | if event.Type == tl.EventKey {
242 | switch event.Key {
243 | case tl.KeyArrowRight:
244 | if snake.direction != "left" {
245 | snake.direction = "right"
246 | }
247 | case tl.KeyArrowLeft:
248 | if snake.direction != "right" {
249 | snake.direction = "left"
250 | }
251 | case tl.KeyArrowUp:
252 | if snake.direction != "down" {
253 | snake.direction = "up"
254 | }
255 | case tl.KeyArrowDown:
256 | if snake.direction != "up" {
257 | snake.direction = "down"
258 | }
259 | }
260 | }
261 |
262 | // Update snake every two ticks
263 | snake.tickCount++
264 | if snake.tickCount >= 2 {
265 | snake.tickCount = 0
266 | newHead := snake.body[0]
267 | // Move head based on the current direction
268 | switch snake.direction {
269 | case "right":
270 | newHead.X += 2
271 | case "left":
272 | newHead.X -= 2
273 | case "up":
274 | newHead.Y -= 1
275 | case "down":
276 | newHead.Y += 1
277 | }
278 |
279 | // Check for food collision
280 | if food.AtPosition(newHead.X, newHead.Y) {
281 | snake.growth += 1
282 | food.placed = false
283 | score++
284 | scoreText.SetText(fmt.Sprintf("Score: %d", score))
285 | for index, mapping := range foodPodMappings {
286 | if mapping.foodEntity == food {
287 | go k8s.DeleteResource(mapping.resourceInfo)
288 | DeletedResourceText(
289 | game.Screen(),
290 | mapping.resourceInfo.Type,
291 | mapping.resourceInfo.Name,
292 | mapping.resourceInfo.Namespace,
293 | 1,
294 | LevelHeight+0,
295 | )
296 | log.Printf("Chaos snake consumed %s: %s in namespace %s\n", mapping.resourceInfo.Type, mapping.resourceInfo.Name, mapping.resourceInfo.Namespace)
297 | foodPodMappings = append(foodPodMappings[:index], foodPodMappings[index+1:]...)
298 | break
299 | }
300 | }
301 | }
302 |
303 | // Grow the snake if needed
304 | if snake.growth > 0 {
305 | snake.body = append([]Coordinates{newHead}, snake.body...)
306 | snake.growth--
307 | } else {
308 | snake.body = append([]Coordinates{newHead}, snake.body[:len(snake.body)-1]...)
309 | }
310 |
311 | // Check for collision with walls or self
312 | if snake.CollidesWithWalls() || snake.CollidesWithSelf() {
313 | GameOver()
314 | }
315 | }
316 | }
317 |
318 | var food *Food
319 | var game *tl.Game
320 | var scoreText *tl.Text
321 | var deletedPodText *tl.Text
322 | var isPaused bool = false
323 | var pauseText *tl.Text
324 |
325 | func main() {
326 | configFilePath := flag.String("config", "", "Path to configuration file")
327 | flag.Parse()
328 |
329 | k8s.SetDefaultConfig()
330 |
331 | // Load configuration from the specified file if provided
332 | if *configFilePath != "" {
333 | err := k8s.LoadConfigFromFile(*configFilePath)
334 | if err != nil {
335 | log.Fatalf("Failed to load config file: %s", err)
336 | }
337 | }
338 |
339 | // init k8s client
340 | k8s.InitKubeClient()
341 |
342 | // Start fetching resources to avoid lag during gameplay
343 | go k8s.FetchResources()
344 |
345 | logFile, err := os.OpenFile("chaos.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
346 | if err != nil {
347 | log.Fatal(err)
348 | }
349 | defer logFile.Close()
350 |
351 | log.SetOutput(logFile)
352 |
353 | game = tl.NewGame()
354 | game.Screen().SetFps(30)
355 |
356 | level := tl.NewBaseLevel(tl.Cell{
357 | Bg: tl.ColorBlack,
358 | Fg: tl.ColorWhite,
359 | Ch: ' ',
360 | })
361 |
362 | snake := NewSnake(20, 20)
363 | food = NewFood()
364 |
365 | // Ensure the first food has pod info ready
366 | foodX := rand.Intn(LevelWidth-4) + 2
367 | foodY := rand.Intn(LevelHeight-4) + 2
368 | food.SetPosition(foodX, foodY)
369 |
370 | select {
371 | case resourceInfo := <-k8s.ResourceInfoQueue:
372 | foodPodMappings = append(foodPodMappings, FoodPodMapping{
373 | foodEntity: food,
374 | resourceInfo: resourceInfo,
375 | })
376 | food.placed = true
377 | case <-time.After(10 * time.Second): // Wait up to 10 seconds
378 | log.Fatal("Failed to fetch initial pod info in time")
379 | }
380 |
381 | level.AddEntity(snake)
382 | level.AddEntity(food)
383 |
384 | scoreText = tl.NewText(1, 0, "Score: 0", tl.ColorWhite, tl.ColorBlack)
385 | deletedPodText = tl.NewText(1, LevelHeight, "", tl.ColorWhite, tl.ColorBlack)
386 | level.AddEntity(scoreText)
387 | level.AddEntity(deletedPodText)
388 |
389 | pauseText = tl.NewText(-1, -1, "GAME PAUSED. Press space to RESUME or CTRL+C to QUIT.", tl.ColorWhite, tl.ColorBlack)
390 | game.Screen().AddEntity(pauseText)
391 |
392 | game.Screen().SetLevel(level)
393 | game.Start()
394 | }
395 |
396 |
--------------------------------------------------------------------------------
/backend/go.sum:
--------------------------------------------------------------------------------
1 | github.com/JoelOtter/termloop v0.0.0-20210806173944-5f7c38744afb h1:wXR5fXM/+4VFARcWVtjwb0wxfQl5RxemkNzzs2Jb918=
2 | github.com/JoelOtter/termloop v0.0.0-20210806173944-5f7c38744afb/go.mod h1:Tie7OOEgasw91JpzA8UywemPyGehxZ06Gqtl5B1/vXI=
3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
9 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
10 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
11 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
12 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
13 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
14 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
15 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
16 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
17 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
18 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
19 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
20 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
21 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
22 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
23 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
24 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
25 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
26 | github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
27 | github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
28 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
29 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
31 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
32 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
33 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
34 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
35 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
36 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
37 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
38 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
39 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
40 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
41 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
42 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
43 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
44 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
45 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
46 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
48 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
49 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
50 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
51 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
52 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
55 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
56 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
57 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
58 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
59 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
60 | github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
61 | github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
62 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
63 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
64 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
65 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
66 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
67 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
68 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
69 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
70 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
71 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
72 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
73 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
74 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
75 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
76 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
77 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
78 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
79 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
80 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
81 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
82 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
83 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
84 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
85 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
86 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
87 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
88 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
89 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
90 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
91 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
92 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
93 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
94 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
95 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
96 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
97 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
98 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
99 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
100 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
101 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
102 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
103 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
104 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
105 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
106 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
107 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
108 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
109 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
110 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
111 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
112 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
113 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
114 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
115 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
116 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
117 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
118 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
119 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
120 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
121 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
122 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
123 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
124 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
125 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
126 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
127 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
128 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
129 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
130 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
131 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
132 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
133 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
134 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
135 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
136 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
137 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
138 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
139 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
140 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
141 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
142 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
143 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
144 | k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
145 | k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
146 | k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
147 | k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
148 | k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
149 | k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
150 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
151 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
152 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
153 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
154 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
155 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
156 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
157 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
158 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
159 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
160 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
161 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
162 | sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
163 | sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
164 |
--------------------------------------------------------------------------------
/backend/pkg/k8s/kubernetes.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "math/rand"
9 | "os"
10 | "path/filepath"
11 | "time"
12 |
13 | v1 "k8s.io/api/core/v1"
14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15 | "k8s.io/client-go/kubernetes"
16 | "k8s.io/client-go/rest"
17 | "k8s.io/client-go/tools/clientcmd"
18 | "k8s.io/client-go/util/homedir"
19 | )
20 |
21 | var ResourceInfoQueue = make(chan ResourceInfo, 100)
22 |
23 | func FetchResources() {
24 | for {
25 | resourceInfo, err := getRandomResourceInfo()
26 | if err != nil {
27 | log.Printf("Error fetching resource info: %s\n", err)
28 | } else {
29 | ResourceInfoQueue <- resourceInfo
30 | }
31 | time.Sleep(1 * time.Second)
32 | }
33 | }
34 |
35 | type KubernetesResource interface {
36 | List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error)
37 | Delete(ctx context.Context, namespace, name string) error
38 | }
39 |
40 | // Resource types supported
41 | // Pods
42 | // ReplicaSets
43 | // Deployments
44 | // StatefulSets
45 | // Services
46 | // DaemonSets
47 | // Secrets
48 | // ConfigMaps
49 | // Jobs
50 | // CronJobs
51 | // Ingresses
52 |
53 | type PodResource struct {
54 | clientset *kubernetes.Clientset
55 | }
56 |
57 | type ReplicaSetResource struct {
58 | clientset *kubernetes.Clientset
59 | }
60 |
61 | type DeploymentResource struct {
62 | clientset *kubernetes.Clientset
63 | }
64 |
65 | type StatefulSetResource struct {
66 | clientset *kubernetes.Clientset
67 | }
68 |
69 | type ServiceResource struct {
70 | clientset *kubernetes.Clientset
71 | }
72 |
73 | type DaemonSetResource struct {
74 | clientset *kubernetes.Clientset
75 | }
76 |
77 | type SecretResource struct {
78 | clientset *kubernetes.Clientset
79 | }
80 |
81 | type ConfigMapResource struct {
82 | clientset *kubernetes.Clientset
83 | }
84 |
85 | type JobResource struct {
86 | clientset *kubernetes.Clientset
87 | }
88 |
89 | type CronJobResource struct {
90 | clientset *kubernetes.Clientset
91 | }
92 |
93 | type IngressResource struct {
94 | clientset *kubernetes.Clientset
95 | }
96 |
97 | type ResourceInfo struct {
98 | Name string
99 | Namespace string
100 | Type string
101 | }
102 |
103 | func (p *PodResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
104 | pods, err := p.clientset.CoreV1().Pods(namespace).List(ctx, opts)
105 | if err != nil {
106 | return nil, err
107 | }
108 | var results []ResourceInfo
109 | for _, pod := range pods.Items {
110 | if !isCriticalPod(pod) {
111 | results = append(results, ResourceInfo{Name: pod.Name, Namespace: pod.Namespace, Type: "pod"})
112 | }
113 | }
114 | return results, nil
115 | }
116 |
117 | func (r *ReplicaSetResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
118 | replicasets, err := r.clientset.AppsV1().ReplicaSets(namespace).List(ctx, opts)
119 | if err != nil {
120 | return nil, err
121 | }
122 | var results []ResourceInfo
123 | for _, rs := range replicasets.Items {
124 | results = append(results, ResourceInfo{Name: rs.Name, Namespace: rs.Namespace, Type: "replicaset"})
125 | }
126 | return results, nil
127 | }
128 |
129 | func (d *DeploymentResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
130 | deployments, err := d.clientset.AppsV1().Deployments(namespace).List(ctx, opts)
131 | if err != nil {
132 | return nil, err
133 | }
134 | var results []ResourceInfo
135 | for _, deployment := range deployments.Items {
136 | results = append(results, ResourceInfo{Name: deployment.Name, Namespace: deployment.Namespace, Type: "deployment"})
137 | }
138 | return results, nil
139 | }
140 |
141 | func (s *StatefulSetResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
142 | statefulSets, err := s.clientset.AppsV1().StatefulSets(namespace).List(ctx, opts)
143 | if err != nil {
144 | return nil, err
145 | }
146 | var results []ResourceInfo
147 | for _, ss := range statefulSets.Items {
148 | results = append(results, ResourceInfo{Name: ss.Name, Namespace: ss.Namespace, Type: "statefulset"})
149 | }
150 | return results, nil
151 | }
152 |
153 | func (s *ServiceResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
154 | services, err := s.clientset.CoreV1().Services(namespace).List(ctx, opts)
155 | if err != nil {
156 | return nil, err
157 | }
158 | var results []ResourceInfo
159 | for _, svc := range services.Items {
160 | results = append(results, ResourceInfo{Name: svc.Name, Namespace: svc.Namespace, Type: "service"})
161 | }
162 | return results, nil
163 | }
164 |
165 | func (d *DaemonSetResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
166 | daemonSets, err := d.clientset.AppsV1().DaemonSets(namespace).List(ctx, opts)
167 | if err != nil {
168 | return nil, err
169 | }
170 | var results []ResourceInfo
171 | for _, ds := range daemonSets.Items {
172 | results = append(results, ResourceInfo{Name: ds.Name, Namespace: ds.Namespace, Type: "daemonset"})
173 | }
174 | return results, nil
175 | }
176 |
177 | func (s *SecretResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
178 | secrets, err := s.clientset.CoreV1().Secrets(namespace).List(ctx, opts)
179 | if err != nil {
180 | return nil, err
181 | }
182 | var results []ResourceInfo
183 | for _, secret := range secrets.Items {
184 | results = append(results, ResourceInfo{Name: secret.Name, Namespace: secret.Namespace, Type: "secret"})
185 | }
186 | return results, nil
187 | }
188 |
189 | func (c *ConfigMapResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
190 | configMaps, err := c.clientset.CoreV1().ConfigMaps(namespace).List(ctx, opts)
191 | if err != nil {
192 | return nil, err
193 | }
194 | var results []ResourceInfo
195 | for _, cm := range configMaps.Items {
196 | results = append(results, ResourceInfo{Name: cm.Name, Namespace: cm.Namespace, Type: "configmap"})
197 | }
198 | return results, nil
199 | }
200 |
201 | func (j *JobResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
202 | jobs, err := j.clientset.BatchV1().Jobs(namespace).List(ctx, opts)
203 | if err != nil {
204 | return nil, err
205 | }
206 | var results []ResourceInfo
207 | for _, job := range jobs.Items {
208 | results = append(results, ResourceInfo{Name: job.Name, Namespace: job.Namespace, Type: "job"})
209 | }
210 | return results, nil
211 | }
212 |
213 | func (c *CronJobResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
214 | cronJobs, err := c.clientset.BatchV1beta1().CronJobs(namespace).List(ctx, opts)
215 | if err != nil {
216 | return nil, err
217 | }
218 | var results []ResourceInfo
219 | for _, cj := range cronJobs.Items {
220 | results = append(results, ResourceInfo{Name: cj.Name, Namespace: cj.Namespace, Type: "cronjob"})
221 | }
222 | return results, nil
223 | }
224 |
225 | func (i *IngressResource) List(ctx context.Context, namespace string, opts metav1.ListOptions) ([]ResourceInfo, error) {
226 | ingresses, err := i.clientset.NetworkingV1().Ingresses(namespace).List(ctx, opts)
227 | if err != nil {
228 | return nil, err
229 | }
230 | var results []ResourceInfo
231 | for _, ing := range ingresses.Items {
232 | results = append(results, ResourceInfo{Name: ing.Name, Namespace: ing.Namespace, Type: "ingress"})
233 | }
234 | return results, nil
235 | }
236 |
237 | func (p *PodResource) Delete(ctx context.Context, namespace, name string) error {
238 | return p.clientset.CoreV1().Pods(namespace).Delete(ctx, name, metav1.DeleteOptions{})
239 | }
240 |
241 | func (r *ReplicaSetResource) Delete(ctx context.Context, namespace, name string) error {
242 | return r.clientset.AppsV1().ReplicaSets(namespace).Delete(ctx, name, metav1.DeleteOptions{})
243 | }
244 |
245 | func (d *DeploymentResource) Delete(ctx context.Context, namespace, name string) error {
246 | return d.clientset.AppsV1().Deployments(namespace).Delete(ctx, name, metav1.DeleteOptions{})
247 | }
248 |
249 | func (s *StatefulSetResource) Delete(ctx context.Context, namespace, name string) error {
250 | return s.clientset.AppsV1().StatefulSets(namespace).Delete(ctx, name, metav1.DeleteOptions{})
251 | }
252 |
253 | func (s *ServiceResource) Delete(ctx context.Context, namespace, name string) error {
254 | return s.clientset.CoreV1().Services(namespace).Delete(ctx, name, metav1.DeleteOptions{})
255 | }
256 |
257 | func (d *DaemonSetResource) Delete(ctx context.Context, namespace, name string) error {
258 | return d.clientset.AppsV1().DaemonSets(namespace).Delete(ctx, name, metav1.DeleteOptions{})
259 | }
260 |
261 | func (s *SecretResource) Delete(ctx context.Context, namespace, name string) error {
262 | return s.clientset.CoreV1().Secrets(namespace).Delete(ctx, name, metav1.DeleteOptions{})
263 | }
264 |
265 | func (c *ConfigMapResource) Delete(ctx context.Context, namespace, name string) error {
266 | return c.clientset.CoreV1().ConfigMaps(namespace).Delete(ctx, name, metav1.DeleteOptions{})
267 | }
268 |
269 | func (j *JobResource) Delete(ctx context.Context, namespace, name string) error {
270 | return j.clientset.BatchV1().Jobs(namespace).Delete(ctx, name, metav1.DeleteOptions{})
271 | }
272 |
273 | func (c *CronJobResource) Delete(ctx context.Context, namespace, name string) error {
274 | return c.clientset.BatchV1beta1().CronJobs(namespace).Delete(ctx, name, metav1.DeleteOptions{})
275 | }
276 |
277 | func (i *IngressResource) Delete(ctx context.Context, namespace, name string) error {
278 | return i.clientset.NetworkingV1().Ingresses(namespace).Delete(ctx, name, metav1.DeleteOptions{})
279 | }
280 |
281 | type Config struct {
282 | ResourceTypes []string `json:"resource_types"`
283 | Namespaces NamespacesConfig `json:"namespaces"`
284 | }
285 |
286 | type NamespacesConfig struct {
287 | Include []string `json:"include"`
288 | Exclude []string `json:"exclude"`
289 | }
290 |
291 | var defaultConfig = Config{
292 | ResourceTypes: []string{"pods"},
293 | Namespaces: NamespacesConfig{
294 | Include: []string{},
295 | Exclude: []string{"kube-system"},
296 | },
297 | }
298 |
299 | var gameConfig Config
300 |
301 | func SetDefaultConfig() {
302 | gameConfig = defaultConfig
303 | }
304 |
305 | func LoadConfigFromFile(filename string) error {
306 | data, err := os.ReadFile(filename)
307 | if err != nil {
308 | return err
309 | }
310 | err = json.Unmarshal(data, &gameConfig)
311 | if err != nil {
312 | return err
313 | }
314 | return nil
315 | }
316 |
317 | func getAllNamespaces() ([]string, error) {
318 | allNamespaces, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
319 | if err != nil {
320 | return nil, err
321 | }
322 |
323 | var filteredNamespaces []string
324 | for _, ns := range allNamespaces.Items {
325 | if (len(gameConfig.Namespaces.Include) == 0 || contains(gameConfig.Namespaces.Include, ns.Name)) &&
326 | !contains(gameConfig.Namespaces.Exclude, ns.Name) {
327 | filteredNamespaces = append(filteredNamespaces, ns.Name)
328 | }
329 | }
330 | return filteredNamespaces, nil
331 | }
332 |
333 |
334 | func contains(slice []string, str string) bool {
335 | for _, v := range slice {
336 | if v == str {
337 | return true
338 | }
339 | }
340 | return false
341 | }
342 |
343 | func getResourceHandler(resourceType string) (KubernetesResource, error) {
344 | switch resourceType {
345 | case "pods":
346 | return &PodResource{clientset: clientset}, nil
347 | case "replicasets":
348 | return &ReplicaSetResource{clientset: clientset}, nil
349 | case "deployments":
350 | return &DeploymentResource{clientset: clientset}, nil
351 | case "statefulsets":
352 | return &StatefulSetResource{clientset: clientset}, nil
353 | case "services":
354 | return &ServiceResource{clientset: clientset}, nil
355 | case "daemonsets":
356 | return &DaemonSetResource{clientset: clientset}, nil
357 | case "secrets":
358 | return &SecretResource{clientset: clientset}, nil
359 | case "configmaps":
360 | return &ConfigMapResource{clientset: clientset}, nil
361 | case "jobs":
362 | return &JobResource{clientset: clientset}, nil
363 | case "cronjobs":
364 | return &CronJobResource{clientset: clientset}, nil
365 | case "ingresses":
366 | return &IngressResource{clientset: clientset}, nil
367 | default:
368 | return nil, fmt.Errorf("unsupported resource type: %s", resourceType)
369 | }
370 | }
371 |
372 | type PodInfo struct {
373 | Name string
374 | Namespace string
375 | }
376 |
377 | var clientset *kubernetes.Clientset
378 |
379 | func InitKubeClient() {
380 | var kubeconfig string
381 | if kc := os.Getenv("KUBECONFIG"); kc != "" {
382 | kubeconfig = kc
383 | } else {
384 | home := homedir.HomeDir()
385 | kubeconfig = filepath.Join(home, ".kube", "config")
386 | }
387 |
388 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
389 | if err != nil {
390 | log.Printf("Error building kubeconfig: %s\n", err.Error())
391 | config, err = rest.InClusterConfig()
392 | if err != nil {
393 | log.Fatalf("Error building kube config: %s\n", err.Error())
394 | }
395 | }
396 |
397 | clientset, err = kubernetes.NewForConfig(config)
398 | if err != nil {
399 | log.Fatalf("Error creating clientset: %s\n", err.Error())
400 | }
401 | }
402 |
403 | func getRandomResourceInfo() (ResourceInfo, error) {
404 | if len(gameConfig.ResourceTypes) == 0 {
405 | return ResourceInfo{}, fmt.Errorf("no resource types configured")
406 | }
407 |
408 | var allResources []ResourceInfo
409 | for _, resourceType := range gameConfig.ResourceTypes {
410 | handler, err := getResourceHandler(resourceType)
411 | if err != nil {
412 | log.Printf("Error getting handler for resource type %s: %s\n", resourceType, err)
413 | continue
414 | }
415 |
416 | filteredNamespaces, err := getAllNamespaces()
417 | if err != nil {
418 | log.Printf("Error fetching namespaces: %s\n", err)
419 | continue
420 | }
421 |
422 | for _, ns := range filteredNamespaces {
423 | resources, err := handler.List(context.TODO(), ns, metav1.ListOptions{})
424 | if err != nil {
425 | log.Printf("Error listing resources in namespace %s: %s\n", ns, err)
426 | continue
427 | }
428 | allResources = append(allResources, resources...)
429 | }
430 | }
431 |
432 | if len(allResources) == 0 {
433 | log.Println("No eligible resources found.")
434 | return ResourceInfo{}, fmt.Errorf("no eligible resources found")
435 | }
436 |
437 | randIndex := rand.Intn(len(allResources))
438 | return allResources[randIndex], nil
439 | }
440 |
441 |
442 | func isCriticalPod(pod v1.Pod) bool {
443 | _, isCritical := pod.Annotations["scheduler.alpha.kubernetes.io/critical-pod"]
444 | return isCritical
445 | }
446 |
447 | func DeleteResource(resourceInfo ResourceInfo) {
448 | var err error
449 | switch resourceInfo.Type {
450 | case "pod":
451 | err = clientset.CoreV1().Pods(resourceInfo.Namespace).Delete(context.TODO(), resourceInfo.Name, metav1.DeleteOptions{})
452 | case "replicaset":
453 | err = clientset.AppsV1().ReplicaSets(resourceInfo.Namespace).Delete(context.TODO(), resourceInfo.Name, metav1.DeleteOptions{})
454 | case "deployment":
455 | err = clientset.AppsV1().Deployments(resourceInfo.Namespace).Delete(context.TODO(), resourceInfo.Name, metav1.DeleteOptions{})
456 | default:
457 | log.Printf("Unsupported resource type: %s", resourceInfo.Type)
458 | return
459 | }
460 |
461 | if err != nil {
462 | log.Printf("Error deleting %s %s in namespace %s: %s\n", resourceInfo.Type, resourceInfo.Name, resourceInfo.Namespace, err.Error())
463 | } else {
464 | log.Printf("%s deleted: %s in namespace %s\n", resourceInfo.Type, resourceInfo.Name, resourceInfo.Namespace)
465 | }
466 | }
467 |
--------------------------------------------------------------------------------