├── .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 |
2 | 3 | go version 4 | 5 |
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 | [![asciicast](https://asciinema.org/a/Q4usmR4HB8LhHojJA9qJeQmdX.svg)](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 | --------------------------------------------------------------------------------