From c9653bd77f80a3e64af1e9dc7e92cd5e86dba9f7 Mon Sep 17 00:00:00 2001 From: skudasov Date: Thu, 22 Aug 2024 11:41:13 +0200 Subject: [PATCH] init, no linters are run --- .github/workflows/havoc-lint.yml | 22 + .github/workflows/havoc-release.yml | 37 + .github/workflows/havoc-test-api.yml | 22 + .prettierignore | 1 + havoc/.gitignore | 59 + havoc/LICENSE | 21 + havoc/Makefile | 24 + havoc/README.md | 171 +++ havoc/cli.go | 209 ++++ havoc/cmd/havoc.go | 13 + havoc/config.go | 350 ++++++ havoc/examples/experiments_test.go | 41 + havoc/flake.lock | 111 ++ havoc/flake.nix | 16 + havoc/generate.go | 1113 +++++++++++++++++ havoc/go.mod | 96 ++ havoc/go.sum | 309 +++++ havoc/havoc.go | 80 ++ havoc/havoc.toml | 141 +++ havoc/havoc_test.go | 134 ++ havoc/img.png | Bin 0 -> 223660 bytes havoc/k8schaos/README.md | 211 ++++ havoc/k8schaos/chaos.go | 615 +++++++++ havoc/k8schaos/chaos_entity.go | 27 + havoc/k8schaos/chaos_helper.go | 44 + havoc/k8schaos/chaos_listener.go | 12 + havoc/k8schaos/console_logger.go | 140 +++ havoc/k8schaos/logger.go | 44 + havoc/k8schaos/range_grafana_annotator.go | 240 ++++ havoc/k8schaos/schedule.go | 225 ++++ .../k8schaos/single_line_grafana_annotator.go | 205 +++ havoc/k8schaos/utils.go | 5 + havoc/monkey.go | 222 ++++ havoc/openapi.go | 140 +++ havoc/oscmd.go | 39 + havoc/parse.go | 181 +++ havoc/shell.nix | 18 + havoc/testdata/configs/crib-all.toml | 126 ++ .../deployments/deployment_crib_1.json | 178 +++ .../deployment_crib_block_rewind.json | 219 ++++ .../deployments/deployment_single_group.json | 20 + .../deployments/deployment_single_pod.json | 12 + havoc/testdata/openapi_specs/petshop.yaml | 119 ++ havoc/testdata/results/.gitkeep | 0 ...nd_head-geth-1337-7f7c9fb6c6-hzdhn-10.yaml | 9 + ...nd_head-geth-1337-7f7c9fb6c6-hzdhn-20.yaml | 9 + ...nd_head-geth-1337-7f7c9fb6c6-hzdhn-30.yaml | 9 + ...nd_head-geth-2337-7f7c9fb6c6-hzdhn-10.yaml | 9 + ...nd_head-geth-2337-7f7c9fb6c6-hzdhn-20.yaml | 9 + ...nd_head-geth-2337-7f7c9fb6c6-hzdhn-30.yaml | 9 + ...app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml | 15 + .../cpu/cpu-mockserver-7cb865999c-qwdt9.yaml | 15 + .../all/cpu/cpu-runner-64c589dd4b-qh4lj.yaml | 15 + ...ster-0a137b375cc3881a70e186ce2172c8d1.yaml | 20 + ...app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml | 12 + .../failure-mockserver-7cb865999c-qwdt9.yaml | 12 + .../failure-runner-64c589dd4b-qh4lj.yaml | 12 + ...oc-component-group-blockchain-1-fixed.yaml | 16 + ...oc-component-group-blockchain-2-fixed.yaml | 16 + ...oc-component-group-blockchain-3-fixed.yaml | 16 + ...pu-havoc-component-group-node-1-fixed.yaml | 16 + ...pu-havoc-component-group-node-2-fixed.yaml | 16 + ...pu-havoc-component-group-node-3-fixed.yaml | 16 + ...oc-component-group-blockchain-1-fixed.yaml | 13 + ...oc-component-group-blockchain-2-fixed.yaml | 13 + ...oc-component-group-blockchain-3-fixed.yaml | 13 + ...re-havoc-component-group-node-1-fixed.yaml | 13 + ...re-havoc-component-group-node-2-fixed.yaml | 13 + ...re-havoc-component-group-node-3-fixed.yaml | 13 + ...oc-component-group-blockchain-1-fixed.yaml | 26 + ...oc-component-group-blockchain-2-fixed.yaml | 26 + ...oc-component-group-blockchain-3-fixed.yaml | 26 + ...cy-havoc-component-group-node-1-fixed.yaml | 26 + ...cy-havoc-component-group-node-2-fixed.yaml | 26 + ...cy-havoc-component-group-node-3-fixed.yaml | 26 + ...oc-component-group-blockchain-1-fixed.yaml | 16 + ...oc-component-group-blockchain-2-fixed.yaml | 16 + ...oc-component-group-blockchain-3-fixed.yaml | 16 + ...ry-havoc-component-group-node-1-fixed.yaml | 16 + ...ry-havoc-component-group-node-2-fixed.yaml | 16 + ...ry-havoc-component-group-node-3-fixed.yaml | 16 + ...p-1-to-havoc-network-group-2-100-perc.yaml | 24 + ...voc-network-group-blockchain-100-perc.yaml | 24 + ...voc-network-group-blockchain-100-perc.yaml | 24 + ...app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml | 24 + .../latency-mockserver-7cb865999c-qwdt9.yaml | 24 + .../latency-runner-64c589dd4b-qh4lj.yaml | 24 + ...app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml | 15 + .../memory-mockserver-7cb865999c-qwdt9.yaml | 15 + .../memory-runner-64c589dd4b-qh4lj.yaml | 15 + ...havoc-component-group-mygroup-1-fixed.yaml | 16 + ...havoc-component-group-mygroup-2-fixed.yaml | 16 + ...havoc-component-group-mygroup-3-fixed.yaml | 16 + ...havoc-component-group-mygroup-1-fixed.yaml | 13 + ...havoc-component-group-mygroup-2-fixed.yaml | 13 + ...havoc-component-group-mygroup-3-fixed.yaml | 13 + ...havoc-component-group-mygroup-1-fixed.yaml | 26 + ...havoc-component-group-mygroup-2-fixed.yaml | 26 + ...havoc-component-group-mygroup-3-fixed.yaml | 26 + ...havoc-component-group-mygroup-1-fixed.yaml | 16 + ...havoc-component-group-mygroup-2-fixed.yaml | 16 + ...havoc-component-group-mygroup-3-fixed.yaml | 16 + .../single_pod/cpu/cpu-my-single-app.yaml | 15 + .../failure/failure-my-single-app.yaml | 12 + .../latency/latency-my-single-app.yaml | 24 + .../memory/memory-my-single-app.yaml | 15 + 106 files changed, 7061 insertions(+) create mode 100644 .github/workflows/havoc-lint.yml create mode 100644 .github/workflows/havoc-release.yml create mode 100644 .github/workflows/havoc-test-api.yml create mode 100644 havoc/.gitignore create mode 100644 havoc/LICENSE create mode 100644 havoc/Makefile create mode 100644 havoc/README.md create mode 100644 havoc/cli.go create mode 100644 havoc/cmd/havoc.go create mode 100644 havoc/config.go create mode 100644 havoc/examples/experiments_test.go create mode 100644 havoc/flake.lock create mode 100644 havoc/flake.nix create mode 100644 havoc/generate.go create mode 100644 havoc/go.mod create mode 100644 havoc/go.sum create mode 100644 havoc/havoc.go create mode 100644 havoc/havoc.toml create mode 100644 havoc/havoc_test.go create mode 100644 havoc/img.png create mode 100644 havoc/k8schaos/README.md create mode 100644 havoc/k8schaos/chaos.go create mode 100644 havoc/k8schaos/chaos_entity.go create mode 100644 havoc/k8schaos/chaos_helper.go create mode 100644 havoc/k8schaos/chaos_listener.go create mode 100644 havoc/k8schaos/console_logger.go create mode 100644 havoc/k8schaos/logger.go create mode 100644 havoc/k8schaos/range_grafana_annotator.go create mode 100644 havoc/k8schaos/schedule.go create mode 100644 havoc/k8schaos/single_line_grafana_annotator.go create mode 100644 havoc/k8schaos/utils.go create mode 100644 havoc/monkey.go create mode 100644 havoc/openapi.go create mode 100644 havoc/oscmd.go create mode 100644 havoc/parse.go create mode 100644 havoc/shell.nix create mode 100644 havoc/testdata/configs/crib-all.toml create mode 100755 havoc/testdata/deployments/deployment_crib_1.json create mode 100755 havoc/testdata/deployments/deployment_crib_block_rewind.json create mode 100755 havoc/testdata/deployments/deployment_single_group.json create mode 100755 havoc/testdata/deployments/deployment_single_pod.json create mode 100644 havoc/testdata/openapi_specs/petshop.yaml create mode 100644 havoc/testdata/results/.gitkeep create mode 100755 havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-10.yaml create mode 100755 havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-20.yaml create mode 100755 havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-30.yaml create mode 100755 havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-10.yaml create mode 100755 havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-20.yaml create mode 100755 havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-30.yaml create mode 100755 havoc/testdata/snapshot/all/cpu/cpu-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml create mode 100755 havoc/testdata/snapshot/all/cpu/cpu-mockserver-7cb865999c-qwdt9.yaml create mode 100755 havoc/testdata/snapshot/all/cpu/cpu-runner-64c589dd4b-qh4lj.yaml create mode 100755 havoc/testdata/snapshot/all/external/external-cl-cluster-0a137b375cc3881a70e186ce2172c8d1.yaml create mode 100755 havoc/testdata/snapshot/all/failure/failure-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml create mode 100755 havoc/testdata/snapshot/all/failure/failure-mockserver-7cb865999c-qwdt9.yaml create mode 100755 havoc/testdata/snapshot/all/failure/failure-runner-64c589dd4b-qh4lj.yaml create mode 100755 havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-1-to-havoc-network-group-2-100-perc.yaml create mode 100755 havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-1-to-havoc-network-group-blockchain-100-perc.yaml create mode 100755 havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-2-to-havoc-network-group-blockchain-100-perc.yaml create mode 100755 havoc/testdata/snapshot/all/latency/latency-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml create mode 100755 havoc/testdata/snapshot/all/latency/latency-mockserver-7cb865999c-qwdt9.yaml create mode 100755 havoc/testdata/snapshot/all/latency/latency-runner-64c589dd4b-qh4lj.yaml create mode 100755 havoc/testdata/snapshot/all/memory/memory-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml create mode 100755 havoc/testdata/snapshot/all/memory/memory-mockserver-7cb865999c-qwdt9.yaml create mode 100755 havoc/testdata/snapshot/all/memory/memory-runner-64c589dd4b-qh4lj.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-1-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-2-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-3-fixed.yaml create mode 100755 havoc/testdata/snapshot/single_pod/cpu/cpu-my-single-app.yaml create mode 100755 havoc/testdata/snapshot/single_pod/failure/failure-my-single-app.yaml create mode 100755 havoc/testdata/snapshot/single_pod/latency/latency-my-single-app.yaml create mode 100755 havoc/testdata/snapshot/single_pod/memory/memory-my-single-app.yaml diff --git a/.github/workflows/havoc-lint.yml b/.github/workflows/havoc-lint.yml new file mode 100644 index 000000000..de73fbb0c --- /dev/null +++ b/.github/workflows/havoc-lint.yml @@ -0,0 +1,22 @@ +name: Havoc Lint +on: + push: +permissions: + contents: read +jobs: + golangci: + defaults: + run: + working-directory: havoc + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.20' + cache: true + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.51.2 diff --git a/.github/workflows/havoc-release.yml b/.github/workflows/havoc-release.yml new file mode 100644 index 000000000..708ce0683 --- /dev/null +++ b/.github/workflows/havoc-release.yml @@ -0,0 +1,37 @@ +on: + release: + types: [created] + +permissions: + contents: write + packages: write + +jobs: + release-linux-amd64: + defaults: + run: + working-directory: havoc + name: release linux/amd64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: linux + goarch: amd64 + project_path: cmd + release-linux-arm64: + defaults: + run: + working-directory: havoc + name: release linux/arm64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: wangyoucao577/go-release-action@v1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + goos: darwin + goarch: arm64 + project_path: cmd diff --git a/.github/workflows/havoc-test-api.yml b/.github/workflows/havoc-test-api.yml new file mode 100644 index 000000000..412c30070 --- /dev/null +++ b/.github/workflows/havoc-test-api.yml @@ -0,0 +1,22 @@ +name: CLI tests +on: + push: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + test: + defaults: + run: + working-directory: havoc + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version-file: 'go.mod' + cache: true + - name: Run tests + run: | + make test diff --git a/.prettierignore b/.prettierignore index 5c08ba825..71b9e910e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,4 @@ k8s-test-runner/chart/**/*.yaml node_modules/ index.yaml wasp/** +havoc/testdata/** \ No newline at end of file diff --git a/havoc/.gitignore b/havoc/.gitignore new file mode 100644 index 000000000..be5b7c479 --- /dev/null +++ b/havoc/.gitignore @@ -0,0 +1,59 @@ +# IDE and environment +.idea/ +.vscode/ +.DS_STORE + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories +dist/ +vendor/ +node_modules/ +.yarn/ + +# Mercuy server configuration file +config.toml +config.*.toml + +# Personal/secret env vars +.envrc-personal + +# Other +tmp/ +*.log +*.swp +.air.toml +.DS_Store +output.txt + +# Default binary name with go build +main +test.sh + +# Default generated experiments +havoc-experiments +havoc-default +experiments-crib-core +testdata/experiments-test +havoc-monkey-temp-dir + +# General env vars config +.envrc + +# Dumps of configs +config_dump.toml +pods_dump.json +testdata/result/** +testdata/results/** +!testdata/results/.gitkeep diff --git a/havoc/LICENSE b/havoc/LICENSE new file mode 100644 index 000000000..98f60070f --- /dev/null +++ b/havoc/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 SmartContract ChainLink, Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/havoc/Makefile b/havoc/Makefile new file mode 100644 index 000000000..fb995d094 --- /dev/null +++ b/havoc/Makefile @@ -0,0 +1,24 @@ +.PHONY: test +test: + go test -v -count 1 `go list ./... | grep -v examples` -run TestSmoke + +.PHONY: test_race +test_race: + go test -v -race -count 1 `go list ./... | grep -v examples` -run TestSmoke + +.PHONY: test+cover +test_cover: + go test -v -coverprofile cover.out -count 1 `go list ./... | grep -v examples` -run "TestSmoke|TestAPI" + go tool cover -html cover.out + +.PHONY: install +install: + go install cmd/havoc.go + +.PHONE: build +build: + go build cmd/havoc.go + +.PHONY: lint +lint: + golangci-lint --color=always run -v diff --git a/havoc/README.md b/havoc/README.md new file mode 100644 index 000000000..26e0d6c56 --- /dev/null +++ b/havoc/README.md @@ -0,0 +1,171 @@ +## Havoc + +_DISCLAIMER_: This software is not even early Alpha, and still in development, use it on your own risk + +Havoc is a tool that introspects your k8s namespace and generates a `ChaosMesh` CRDs suite for you + +You can use havoc as a CLI to quickly test hypothesis or run it in "monkey" mode with your load tests and have Grafana annotations + +### How it works + +![img.png](img.png) + +Havoc generates groups of experiments based on your pods and labels found in namespace + +In order to test your namespace you need to label pods accordingly: + +- `havoc-component-group` is like `app.kubernetes.io/component` (see [recommendation](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/)) but should be set explicitly +- `havoc-network-group` in most cases match `havoc-component-group` but sometimes you need to break network even inside component group, ex. distributed databases + +Example: + +``` + havoc-component-group: node + havoc-network-group: nodes-1 +``` + +Every pod without a group will be marked as `no-group` and experiments will be assigned accordingly + +Single pod experiments: + +- PodFailure +- NetworkChaos (Pod latency) +- Stress (Memory) +- Stress (CPU) +- External service failure (Network partition) +- Blockchain specific experiments + +Group experiments: + +- Group failure +- Group latency +- Group CPU +- Group memory +- Group network partition +- OpenAPI based HTTP experiments + +You can generate default chaos suite by [configuring](havoc.toml) havoc then set `dir` param and add your custom experiments, then run monkey to test your services + +### Why use it? + +#### Without Havoc your workflow is + +- Inspect full rendered deployment of your namespace +- Figure out multiple groups of components you can select by various labels or annotations to form experiments +- If some components are not selectable - ask DevOps guys to change the manifests +- Create set of experiments for each chaos experiment type by hand or copy from other product chaos tests +- Calculate permutations of different groups and calculate composite experiments (network partitioning, latency) +- Create experiment for each API in every OpenAPI spec +- Compose huge ChaosMesh Workflow YAML that fails without proper validation errors if group has no match or label is invalid +- Run the load test, then manually run the chaos suite +- Check experiment logs to debug with kubectl +- Figure out which failures are caused by which experiments +- If you have more than one project, use some templating make experiments work for other projects + +#### With Havoc + +- Have a simple labelling convention for your namespaces, fill 5 vars in `TOML` config +- Run chaos testing with `havoc -c havoc.toml run ${namespace}` + +### Install + +Please use GitHub releases of this repo +Download latest [release](https://github.com/smartcontractkit/havoc/releases) + +You need `kubectl` to available on your machine + +You also need [ChaosMesh](https://chaos-mesh.org/) installed in your `k8s` cluster + +### Grafana integration + +Set env variables + +``` +HAVOC_LOG_LEVEL={warn,info,debug,trace} +GRAFANA_URL="..." +GRAFANA_TOKEN="..." +``` + +Set dashboard names in `havoc.toml` + +``` +[havoc.grafana] +# UIDs of dashboard which should be annotated with chaos experiments metadata +# You can also try to use name as you see it in the top bar of your dashboard but that's not guaranteed to match +dashboard_uids = ["WaspDebug", "e98b5451-12dc-4a8b-9576-2c0b67ddbd0c"] +``` + +### Manual usage + +Generate default experiments for your namespace + +``` +havoc -c havoc.toml generate [namespace] +``` + +Check this [section](havoc.toml) for `ignore_pods` and `ignore_group_labels`, default settings should be reasonable, however, you can tweak them + +This will create `havoc-experiments` dir, then you can choose from recommended experiments + +``` +havoc -c havoc.toml apply +``` + +You can also apply your experiment directly, using absolute or relative path to experiment file + +``` +havoc -c havoc.toml apply ${experiment_file_path} +``` + +### Monkey mode + +You can run havoc as an automated sequential or randomized suite + +``` +havoc -c havoc.toml run [namespace] +``` + +See `[havoc.monkey]` config [here](havoc.toml) + +### Programmatic usage + +See how you can use recommended experiments from code in [examples](examples) + +### Custom experiments + +Havoc is just a generator and a module that reads your `dir = $mydir` from config + +If you wish to add custom experiments written by hand create your custom directory and add experiments + +Experiments will be executed in lexicographic order, however, for custom experiments there are 2 simple rules: + +- directory names must be in + +``` + "external", + "failure", + "latency", + "cpu", + "memory", + "group-failure", + "group-latency", + "group-cpu", + "group-memory", + "group-partition", + "blockchain_rewind_head", + "http" +``` + +- `metadata.name` should be equal to your experiment filename + +When you are using `run` monkey command, if directory is not empty havoc won't automatically generate experiments, so you can extend generated experiments with your custom modifications + +### Developing + +We are using [nix](https://nixos.org/) + +Enter the shell + +``` +nix develop +``` diff --git a/havoc/cli.go b/havoc/cli.go new file mode 100644 index 000000000..d240820d9 --- /dev/null +++ b/havoc/cli.go @@ -0,0 +1,209 @@ +package havoc + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/c-bata/go-prompt" + "github.com/pkg/errors" + "github.com/urfave/cli/v2" +) + +const ( + DefaultCMDTimeout = "3m" + + ErrNoSelection = "no selection, exiting" + ErrInvalidNamespace = "first argument must be a valid k8s namespace" + ErrAutocompleteError = "autocomplete file walk errored" +) + +func experimentCompleter(dir string, expType string) (func(d prompt.Document) []prompt.Suggest, error) { + s := make([]prompt.Suggest, 0) + err := filepath.Walk( + fmt.Sprintf("%s/%s", dir, expType), + func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + s = append(s, prompt.Suggest{ + Text: info.Name(), + Description: info.Name(), + }) + return nil + }) + if err != nil { + return nil, err + } + return func(d prompt.Document) []prompt.Suggest { + return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) + }, nil +} + +func experimentTypeCompleter(dir string) (func(d prompt.Document) []prompt.Suggest, error) { + s := make([]prompt.Suggest, 0) + err := filepath.Walk( + dir, + func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.Name() != dir { + s = append(s, prompt.Suggest{ + Text: info.Name(), + Description: info.Name(), + }) + } + return nil + }) + if err != nil { + return nil, err + } + return func(d prompt.Document) []prompt.Suggest { + return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) + }, nil +} + +func RunCLI(args []string) error { + app := &cli.App{ + EnableBashCompletion: true, + Name: "havoc", + Version: "v0.0.1", + Usage: "Automatic chaos experiments CLI", + UsageText: `Utility to generate and apply chaos experiments for a namespace`, + Before: func(cCtx *cli.Context) error { + InitDefaultLogging() + return nil + }, + Flags: []cli.Flag{ + &cli.StringFlag{Name: "config", Aliases: []string{"c"}}, + }, + Commands: []*cli.Command{ + { + Name: "generate", + HelpName: "generate", + Aliases: []string{"g"}, + Description: `generates chaos experiments: +havoc generate [namespace] +or use custom config +havoc -c havoc.toml generate [namespace] +you can also specify a directory where to put manifests +havoc -c havoc.toml -d custom_experiments [namespace] +`, + Action: func(cliCtx *cli.Context) error { + ns := cliCtx.Args().Get(0) + if ns == "" { + return errors.New(ErrInvalidNamespace) + } + cfg, err := ReadConfig(cliCtx.String("config")) + if err != nil { + return err + } + m, err := NewController(cfg) + if err != nil { + return err + } + return m.GenerateSpecs(ns) + }, + }, + { + Name: "apply", + HelpName: "apply", + Aliases: []string{"a"}, + Description: `applies an experiment from a file: +examples: +# selecting an experiment +havoc -c havoc.toml apply +# applying experiment directly with relative or abs path +havoc apply ${experiment_path} +`, + Action: func(cliCtx *cli.Context) error { + cfg, err := ReadConfig(cliCtx.String("config")) + if err != nil { + return err + } + m, err := NewController(cfg) + if err != nil { + return err + } + cc, err := experimentTypeCompleter(m.cfg.Havoc.Dir) + if err != nil { + return err + } + + var expPath string + + arg := cliCtx.Args().Get(0) + + if arg != "" { + expPath = arg + } else { + expType := prompt.Input("Choose experiment type >> ", cc) + if expType == "" { + return errors.New(ErrNoSelection) + } + c, err := experimentCompleter(m.cfg.Havoc.Dir, expType) + if err != nil { + return errors.Wrap(err, ErrAutocompleteError) + } + expName := prompt.Input("Choose experiment name >> ", c) + if expName == "" { + return errors.New(ErrNoSelection) + } + expPath = fmt.Sprintf("%s/%s/%s", cfg.Havoc.Dir, expType, expName) + } + + nexp, err := NewNamedExperiment(expPath) + if err != nil { + return err + } + + return m.ApplyAndAnnotate(nexp) + }, + }, + { + Name: "run", + HelpName: "run", + Aliases: []string{"r"}, + Description: `starts a chaos monkey +examples: +havoc run -c havoc.toml [namespace] +`, + Action: func(cliCtx *cli.Context) error { + ns := cliCtx.Args().Get(0) + cfgPath := cliCtx.String("config") + cfg, err := ReadConfig(cfgPath) + if err != nil { + return err + } + m, err := NewController(cfg) + if err != nil { + return err + } + if _, err := os.Stat(cfg.Havoc.Dir); err != nil { + L.Info(). + Str("Dir", cfg.Havoc.Dir). + Msg("Dir not found, generating specified experiments directory") + err = m.GenerateSpecs(ns) + if err != nil { + return err + } + L.Info(). + Str("Dir", cfg.Havoc.Dir). + Msg("Using existing experiments dir, skipping generation") + } else { + L.Info(). + Str("Dir", cfg.Havoc.Dir). + Msg("Using existing experiments dir, skipping generation") + } + return m.Run() + }, + }, + }, + } + return app.Run(args) +} diff --git a/havoc/cmd/havoc.go b/havoc/cmd/havoc.go new file mode 100644 index 000000000..eeb809d9f --- /dev/null +++ b/havoc/cmd/havoc.go @@ -0,0 +1,13 @@ +package main + +import ( + "os" + + "github.com/smartcontractkit/chainlink-testing-framework/havoc" +) + +func main() { + if err := havoc.RunCLI(os.Args); err != nil { + havoc.L.Fatal().Err(err).Send() + } +} diff --git a/havoc/config.go b/havoc/config.go new file mode 100644 index 000000000..2d0c33d4f --- /dev/null +++ b/havoc/config.go @@ -0,0 +1,350 @@ +package havoc + +import ( + "github.com/pelletier/go-toml/v2" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "os" + "strings" +) + +const ( + ErrReadSethConfig = "failed to read TOML config for havoc" + ErrUnmarshalSethConfig = "failed to unmarshal TOML config for havoc" + + ErrFailureGroupIsNil = "failure group must be specified in config" + ErrLatencyGroupIsNil = "latency group must be specified in config" + ErrStressCPUGroupIsNil = "stress cpu group must be specified in config" + ErrStressMemoryGroupIsNil = "stress memory group must be specified in config" + ErrFormat = "format error" +) + +const ( + DefaultExperimentsDir = "havoc-experiments" + DefaultPodFailureDuration = "1m" + DefaultNetworkLatencyDuration = "1m" + DefaultNetworkPartitionDuration = "1m" + DefaultHTTPDuration = "1m" + DefaultNetworkPartitionLabel = "havoc-network-group" + DefaultComponentGroupLabelKey = "havoc-component-group" + DefaultStressMemoryDuration = "1m" + DefaultStressMemoryWorkers = 1 + DefaultStressMemoryAmount = "512MB" + DefaultStressCPUDuration = "1m" + DefaultStressCPUWorkers = 1 + DefaultStressCPULoad = 100 + DefaultNetworkLatency = "300ms" + DefaultMonkeyDuration = "24h" + DefaultMonkeyMode = "seq" + DefaultMonkeyCooldown = "30s" +) + +var ( + DefaultGroupPercentage = []string{"10", "20", "30"} + DefaultGroupFixed = []string{"1", "2", "3"} + DefaultNetworkPartitionGroupPercentage = []string{"100"} +) + +var ( + DefaultIgnoreGroupLabels = []string{ + "mainnet", + "release", + "intents.otterize.com", + "pod-template-hash", + "rollouts-pod-template-hash", + "chain.link/app", + "chain.link/cost-center", + "chain.link/env", + "chain.link/project", + "chain.link/team", + "app.kubernetes.io/part-of", + "app.kubernetes.io/managed-by", + "app.chain.link/product", + "app.kubernetes.io/version", + "app.chain.link/blockchain", + "app.kubernetes.io/instance", + "app.kubernetes.io/name", + } +) + +type Config struct { + Havoc *Havoc `toml:"havoc"` +} + +type Havoc struct { + Dir string `toml:"dir"` + ExperimentTypes []string `toml:"experiment_types"` + NamespaceLabelFilter string `toml:"namespace_label_filter"` + ComponentLabelKey string `toml:"component_label_key"` + IgnoredPods []string `toml:"ignore_pods"` + IgnoreGroupLabels []string `toml:"ignore_group_labels"` + Failure *Failure `toml:"failure"` + Latency *Latency `toml:"latency"` + NetworkPartition *NetworkPartition `toml:"network_partition"` + StressMemory *StressMemory `toml:"stress_memory"` + StressCPU *StressCPU `toml:"stress_cpu"` + ExternalTargets *ExternalTargets `toml:"external_targets"` + BlockchainRewindHead *BlockchainRewindHead `toml:"blockchain_rewind_head"` + OpenAPI *OpenAPI `toml:"openapi"` + Monkey *Monkey `toml:"monkey"` + Grafana *Grafana `toml:"grafana"` +} + +func dumpConfig(cfg *Config) { + if L.GetLevel() == zerolog.DebugLevel { + d, _ := toml.Marshal(cfg) + _ = os.WriteFile("config_dump.toml", d, os.ModePerm) + } +} + +func DefaultConfig() *Config { + return &Config{ + Havoc: &Havoc{ + Dir: DefaultExperimentsDir, + ExperimentTypes: RecommendedExperimentTypes, + ComponentLabelKey: DefaultComponentGroupLabelKey, + IgnoreGroupLabels: DefaultIgnoreGroupLabels, + Failure: &Failure{ + Duration: DefaultPodFailureDuration, + GroupFixed: DefaultGroupFixed, + }, + Latency: &Latency{ + Duration: DefaultNetworkLatencyDuration, + Latency: DefaultNetworkLatency, + GroupFixed: DefaultGroupFixed, + }, + StressMemory: &StressMemory{ + Duration: DefaultStressMemoryDuration, + Workers: DefaultStressMemoryWorkers, + Memory: DefaultStressMemoryAmount, + GroupFixed: DefaultGroupFixed, + }, + StressCPU: &StressCPU{ + Duration: DefaultStressCPUDuration, + Workers: DefaultStressCPUWorkers, + Load: DefaultStressCPULoad, + GroupFixed: DefaultGroupFixed, + }, + NetworkPartition: &NetworkPartition{ + Duration: DefaultNetworkPartitionDuration, + Label: DefaultNetworkPartitionLabel, + GroupPercentage: DefaultNetworkPartitionGroupPercentage, + }, + OpenAPI: &OpenAPI{ + Duration: DefaultHTTPDuration, + GroupFixed: DefaultGroupFixed, + }, + Monkey: &Monkey{ + Duration: DefaultMonkeyDuration, + Mode: DefaultMonkeyMode, + Cooldown: DefaultMonkeyCooldown, + }, + Grafana: &Grafana{ + URL: os.Getenv("GRAFANA_URL"), + Token: os.Getenv("GRAFANA_TOKEN"), + }, + }, + } +} + +func (c *Config) Validate() []error { + errs := make([]error, 0) + if c.Havoc.Dir == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "monkey.dir must not be empty")) + } + if c.Havoc.Failure == nil { + errs = append(errs, errors.New(ErrFailureGroupIsNil)) + } + if c.Havoc.Latency == nil { + errs = append(errs, errors.New(ErrLatencyGroupIsNil)) + } + if c.Havoc.StressCPU == nil { + errs = append(errs, errors.New(ErrStressCPUGroupIsNil)) + } + if c.Havoc.StressMemory == nil { + errs = append(errs, errors.New(ErrStressMemoryGroupIsNil)) + } + if c.Havoc.Failure != nil { + if c.Havoc.Failure.Duration == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "failure.duration must be in Go duration format, 1d2h3m0s")) + } + } + if c.Havoc.Latency != nil { + if c.Havoc.Latency.Duration == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "latency.duration must be in Go duration format, 1d2h3m0s")) + } + if c.Havoc.Latency.Latency == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "latency.latency must be in milliseconds format, ex.: 300ms")) + } + } + if c.Havoc.StressMemory != nil { + if c.Havoc.StressMemory.Workers <= 0 { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "stress_memory.workers must be set, ex.: \"4\"")) + } + if c.Havoc.StressMemory.Memory == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "stress_memory.memory must be set, ex.: \"256MB\" or \"25%\"")) + } + } + if c.Havoc.StressCPU != nil { + if c.Havoc.StressCPU.Workers <= 0 { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "stress_cpu.workers must be set, ex.: \"1\"")) + } + if c.Havoc.StressCPU.Load <= 0 { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "stress_cpu.load must be set, ex.: \"100\"")) + } + } + if c.Havoc.BlockchainRewindHead != nil { + if c.Havoc.BlockchainRewindHead.Duration == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "havoc.blockchain_rewind_head.duration must be set, ex.: \"30s\"")) + } + for _, bn := range c.Havoc.BlockchainRewindHead.NodesConfig { + if bn.ExecutorPodPrefix == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "havoc.blockchain_rewind_head.nodes.executor_pod_prefix must be set, ex.: \"geth\"")) + } + if bn.ExecutorContainerName == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "havoc.blockchain_rewind_head.nodes.executor_container_name must be set, ex.: \"geth-network\"")) + } + if bn.NodeInternalHTTPURL == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "havoc.blockchain_rewind_head.nodes.node_internal_http_url must be set, ex.: \"geth-1337:8544\"")) + } + if len(bn.Blocks) == 0 { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "havoc.blockchain_rewind_head.nodes.blocks must be set, ex.: \"10\"")) + } + } + } + if c.Havoc.Monkey != nil { + if c.Havoc.Monkey.Mode == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "monkey.mode must be either \"seq\" or \"rand\"")) + } + if c.Havoc.Monkey.Duration == "" { + errs = append(errs, errors.Wrap(errors.New(ErrFormat), "monkey.duration must be in Go duration format, 1d2h3m0s")) + } + } + return errs +} + +type Failure struct { + Duration string `toml:"duration"` + GroupPercentage []string `toml:"group_percentage"` + GroupFixed []string `toml:"group_fixed"` +} + +type Latency struct { + Duration string `toml:"duration"` + Latency string `toml:"latency"` + GroupPercentage []string `toml:"group_percentage"` + GroupFixed []string `toml:"group_fixed"` +} + +type NetworkPartition struct { + Duration string `toml:"duration"` + Label string `toml:"label"` + GroupPercentage []string `toml:"group_percentage"` + GroupFixed []string `toml:"group_fixed"` +} + +type StressMemory struct { + Duration string `toml:"duration"` + Workers int `toml:"workers"` + Memory string `toml:"memory"` + GroupPercentage []string `toml:"group_percentage"` + GroupFixed []string `toml:"group_fixed"` +} + +type StressCPU struct { + Duration string `toml:"duration"` + Workers int `toml:"workers"` + Load int `toml:"load"` + GroupPercentage []string `toml:"group_percentage"` + GroupFixed []string `toml:"group_fixed"` +} + +type ExternalTargets struct { + Duration string `toml:"duration"` + URLs []string `toml:"urls"` +} + +type BlockchainRewindHead struct { + Duration string `toml:"duration"` + NodesConfig []*BlockchainNodeConfig `toml:"nodes"` +} + +type BlockchainNodeConfig struct { + ExecutorPodPrefix string `toml:"executor_pod_prefix"` + ExecutorContainerName string `toml:"executor_container_name"` + NodeInternalHTTPURL string `toml:"node_internal_http_url"` + Blocks []int64 `toml:"blocks"` +} + +type OpenAPI struct { + Mapping map[string]*OpenApiSpecInfo `toml:"mapping"` + Duration string `toml:"duration"` + GroupPercentage []string `toml:"group_percentage"` + GroupFixed []string `toml:"group_fixed"` +} + +type OpenApiSpecInfo struct { + SpecToPortMappings []*SpecToPort `toml:"spec_to_port"` +} + +type SpecToPort struct { + Port int64 `toml:"port"` + Path string `toml:"path"` +} + +type Monkey struct { + Duration string `toml:"duration"` + Cooldown string `toml:"cooldown"` + Mode string `toml:"mode"` +} + +type Grafana struct { + URL string `toml:"grafana_url"` + Token string `toml:"grafana_token"` + DashboardUIDs []string `toml:"dashboard_uids"` +} + +func ReadConfig(path string) (*Config, error) { + cfg := DefaultConfig() + dumpConfig(cfg) + if path == "" { + L.Info().Msg("No config specified, using default configuration") + } else { + L.Debug(). + Str("Path", path). + Msg("Reading config from path") + d, err := os.ReadFile(path) + if err != nil { + return nil, errors.Wrap(err, ErrReadSethConfig) + } + err = toml.Unmarshal(d, &cfg) + if err != nil { + return nil, errors.Wrap(err, ErrUnmarshalSethConfig) + } + } + L.Debug(). + Interface("Config", cfg). + Msg("Configuration loaded") + cfg.Havoc.Grafana.URL = os.Getenv("GRAFANA_URL") + cfg.Havoc.Grafana.Token = os.Getenv("GRAFANA_TOKEN") + return cfg, nil +} + +// nolint +func sliceContains(target string, array []string) bool { + for _, element := range array { + if element == target { + return true + } + } + return false +} + +func sliceContainsSubString(target string, array []string) bool { + for _, element := range array { + if strings.Contains(target, element) { + return true + } + } + return false +} diff --git a/havoc/examples/experiments_test.go b/havoc/examples/experiments_test.go new file mode 100644 index 000000000..f82051af2 --- /dev/null +++ b/havoc/examples/experiments_test.go @@ -0,0 +1,41 @@ +package havoc_example + +import ( + "github.com/rs/zerolog" + "github.com/smartcontractkit/chainlink-testing-framework/havoc" + "github.com/stretchr/testify/require" + "testing" +) + +func createMonkey(t *testing.T, l zerolog.Logger, namespace string) *havoc.Controller { + havoc.SetGlobalLogger(l) + cfg, err := havoc.ReadConfig("config.toml") + require.NoError(t, err) + c, err := havoc.NewController(cfg) + err = c.GenerateSpecs(namespace) + require.NoError(t, err) + return c +} + +func TestMyLoad(t *testing.T) { + /* my testing logger */ + l := havoc.L + /* my load test preparation here */ + /* wrapping with chaos monkey */ + monkey := createMonkey(t, l, "my namespace, get it from config") + go monkey.Run() + /* my test runs and ends */ + errs := monkey.Stop() + require.Len(t, errs, 0) +} + +func TestCodeRun(t *testing.T) { + cfg, err := havoc.ReadConfig("../havoc.toml") + require.NoError(t, err) + c, err := havoc.NewController(cfg) + require.NoError(t, err) + nexp, err := havoc.NewNamedExperiment("../experiments-crib-core/failure/failure-app-node-1-bootstrap-69fb558d9-s7npw.yaml") + require.NoError(t, err) + err = c.ApplyAndAnnotate(nexp) + require.NoError(t, err) +} diff --git a/havoc/flake.lock b/havoc/flake.lock new file mode 100644 index 000000000..cc78f23f2 --- /dev/null +++ b/havoc/flake.lock @@ -0,0 +1,111 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1644229661, + "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "foundry": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1704359589, + "narHash": "sha256-kBFwc8WgQq57TLtOuU7yRoos9S3/P6eZO28xNEqOmH4=", + "owner": "shazow", + "repo": "foundry.nix", + "rev": "c5090280d94328924eddca6939e82216183a9461", + "type": "github" + }, + "original": { + "owner": "shazow", + "ref": "monthly", + "repo": "foundry.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1666753130, + "narHash": "sha256-Wff1dGPFSneXJLI2c0kkdWTgxnQ416KE6X4KnFkgPYQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f540aeda6f677354f1e7144ab04352f61aaa0118", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1704194953, + "narHash": "sha256-RtDKd8Mynhe5CFnVT8s0/0yqtWFMM9LmCzXv/YKxnq4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "bd645e8668ec6612439a9ee7e71f7eac4099d4f6", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "foundry": "foundry", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/havoc/flake.nix b/havoc/flake.nix new file mode 100644 index 000000000..b8cd5f9bd --- /dev/null +++ b/havoc/flake.nix @@ -0,0 +1,16 @@ +{ + description = "havoc"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + foundry.url = "github:shazow/foundry.nix/monthly"; # Use monthly branch for permanent releases + }; + + outputs = inputs@{ self, nixpkgs, flake-utils, foundry, ... }: + flake-utils.lib.eachDefaultSystem (system: + let pkgs = import nixpkgs { inherit system; overlays = [ foundry.overlay ]; }; + in rec { + devShell = pkgs.callPackage ./shell.nix {}; + }); +} diff --git a/havoc/generate.go b/havoc/generate.go new file mode 100644 index 000000000..bc036a8f8 --- /dev/null +++ b/havoc/generate.go @@ -0,0 +1,1113 @@ +package havoc + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/samber/lo" + "io/fs" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "text/template" + "time" + + "github.com/google/uuid" + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +const ( + ErrParsingTemplate = "failed to parse Go text template" + + ErrExperimentTimeout = "waiting for experiment to finish timed out" + ErrExperimentApply = "error applying experiment manifest" + ErrInvalidCustomKind = "invalid custom Kind of experiment" +) + +const ( + DebugContainerImage = "curlimages/curl:latest" +) + +var ( + RecommendedExperimentTypes = []string{ + ChaosTypeFailure, + ChaosTypeLatency, + ChaosTypeGroupFailure, + ChaosTypeGroupLatency, + ChaosTypeStressMemory, + ChaosTypeStressGroupMemory, + ChaosTypeStressCPU, + ChaosTypeStressGroupCPU, + ChaosTypePartitionGroup, + ChaosTypeHTTP, + //ChaosTypePartitionExternal, + } +) + +// MarshalTemplate Helper to marshal templates +func MarshalTemplate(jobSpec interface{}, name, templateString string) (string, error) { + var buf bytes.Buffer + tmpl, err := template.New(name).Parse(templateString) + if err != nil { + return "", errors.Wrap(err, ErrParsingTemplate) + } + err = tmpl.Execute(&buf, jobSpec) + if err != nil { + return "", err + } + return buf.String(), err +} + +type CommonExperimentMeta struct { + Kind string `yaml:"kind"` + Metadata struct { + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + } `yaml:"metadata"` +} + +type HTTPExperiment struct { + ExperimentName string + Metadata *Metadata + Namespace string + Mode string + ModeValue string + Selector string + PodName string + Port int64 + Target string + Path string + Method string + Abort bool + Duration string +} + +func (m HTTPExperiment) String() (string, error) { + tpl := ` +kind: HTTPChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: {{ .ExperimentName }} +spec: + mode: {{ .Mode }} + {{- if .ModeValue }} + value: '{{ .ModeValue }}' + {{- end }} + selector: + namespaces: + - {{ .Namespace }} + {{- if .Selector}} + labelSelectors: + {{ .Selector }} + {{- else}} + fieldSelectors: + metadata.name: {{ .PodName }} + {{- end}} + target: Request + port: {{ .Port }} + method: {{ .Method }} + path: {{ .Path }} + abort: {{ .Abort }} + duration: {{ .Duration }} +` + return MarshalTemplate( + m, + uuid.NewString(), + tpl, + ) +} + +type BlockchainRewindHeadExperiment struct { + ExperimentName string `yaml:"experimentName"` + Metadata *Metadata `yaml:"metadata"` + Namespace string `yaml:"namespace"` + PodName string `yaml:"podName"` + ExecutorPodPrefix string `yaml:"executorPodPrefix"` + ExecutorContainerName string `yaml:"executorContainerName"` + NodeInternalHTTPURL string `yaml:"nodeInternalHTTPURL"` + Blocks int64 `yaml:"blocks"` +} + +type Metadata struct { + Name string `json:"name"` + Labels map[string]string `json:"labels"` +} + +func (m BlockchainRewindHeadExperiment) String() (string, error) { + tpl := ` +kind: blockchain_rewind_head +name: {{ .ExperimentName }} +metadata: + name: {{ .Metadata.Name }} +podName: {{ .PodName }} +executorContainerName: {{ .ExecutorContainerName }} +nodeInternalHTTPURL: {{ .NodeInternalHTTPURL }} +namespace: {{ .Namespace }} +blocks: {{ .Blocks }} +` + return MarshalTemplate( + m, + uuid.NewString(), + tpl, + ) +} + +type NetworkChaosExperiment struct { + ExperimentName string + Mode string + ModeValue string + Namespace string + Duration string + Latency string + PodName string + Selector string +} + +func (m NetworkChaosExperiment) String() (string, error) { + tpl := ` +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: {{ .ExperimentName }} + namespace: {{ .Namespace }} +spec: + selector: + namespaces: + - {{ .Namespace }} + {{- if .Selector}} + labelSelectors: + {{ .Selector }} + {{- else}} + fieldSelectors: + metadata.name: {{ .PodName }} + {{- end}} + mode: {{ .Mode }} + {{- if .ModeValue }} + value: '{{ .ModeValue }}' + {{- end }} + action: delay + duration: {{ .Duration }} + delay: + latency: {{ .Latency }} + direction: from + target: + selector: + namespaces: + - {{ .Namespace }} + {{- if .Selector}} + labelSelectors: + {{ .Selector }} + {{- else}} + fieldSelectors: + metadata.name: {{ .PodName }} + {{- end}} + mode: {{ .Mode }} + {{- if .ModeValue }} + value: '{{ .ModeValue }}' + {{- end }} +` + return MarshalTemplate( + m, + uuid.NewString(), + tpl, + ) +} + +type NetworkChaosGroupPartitionExperiment struct { + ExperimentName string + ModeTo string + ModeToValue string + ModeFrom string + ModeFromValue string + Direction string + Namespace string + Duration string + SelectorFrom string + SelectorTo string +} + +func (m NetworkChaosGroupPartitionExperiment) String() (string, error) { + tpl := ` +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: {{ .ExperimentName }} + namespace: {{ .Namespace }} +spec: + selector: + namespaces: + - {{ .Namespace }} + labelSelectors: + {{ .SelectorFrom }} + action: partition + mode: {{ .ModeFrom }} + {{- if .ModeFromValue }} + value: '{{ .ModeFromValue }}' + {{- end }} + duration: {{ .Duration }} + direction: {{ .Direction }} + target: + mode: {{ .ModeTo }} + {{- if .ModeToValue }} + value: '{{ .ModeToValue }}' + {{- end }} + selector: + namespaces: + - {{ .Namespace }} + labelSelectors: + {{ .SelectorTo }} +` + return MarshalTemplate( + m, + uuid.NewString(), + tpl, + ) +} + +type NetworkChaosExternalPartitionExperiment struct { + ExperimentName string + Namespace string + Duration string + PodName string + ExternalURL string +} + +func (m NetworkChaosExternalPartitionExperiment) String() (string, error) { + tpl := ` +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: {{ .ExperimentName }} + namespace: {{ .Namespace }} +spec: + selector: + namespaces: + - {{ .Namespace }} + mode: all + action: partition + duration: {{ .Duration }} + direction: to + target: + selector: + namespaces: + - {{ .Namespace }} + mode: all + externalTargets: + - {{ .ExternalURL }} +` + return MarshalTemplate( + m, + uuid.NewString(), + tpl, + ) +} + +type PodFailureExperiment struct { + ExperimentName string + Mode string + ModeValue string + Namespace string + Duration string + PodName string + Selector string +} + +func (m PodFailureExperiment) String() (string, error) { + tpl := ` +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: {{ .ExperimentName }} + namespace: {{ .Namespace }} +spec: + action: pod-failure + mode: {{ .Mode }} + {{- if .ModeValue }} + value: '{{ .ModeValue }}' + {{- end }} + duration: {{ .Duration }} + selector: + {{- if .Selector}} + labelSelectors: + {{ .Selector }} + {{- else}} + fieldSelectors: + metadata.name: {{ .PodName }} + {{- end}} +` + return MarshalTemplate( + m, + uuid.NewString(), + tpl, + ) +} + +type PodStressCPUExperiment struct { + ExperimentName string + Mode string + ModeValue string + Namespace string + Workers int + Load int + Duration string + PodName string + Selector string +} + +func (m PodStressCPUExperiment) String() (string, error) { + tpl := ` +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: {{ .ExperimentName }} + namespace: {{ .Namespace }} +spec: + mode: {{ .Mode }} + {{- if .ModeValue }} + value: '{{ .ModeValue }}' + {{- end }} + duration: {{ .Duration }} + selector: + {{- if .Selector}} + labelSelectors: + {{ .Selector }} + {{- else}} + fieldSelectors: + metadata.name: {{ .PodName }} + {{- end}} + stressors: + cpu: + workers: {{ .Workers }} + load: {{ .Load }} +` + return MarshalTemplate( + m, + uuid.NewString(), + tpl, + ) +} + +type PodStressMemoryExperiment struct { + ExperimentName string + Mode string + ModeValue string + Namespace string + Workers int + Memory string + Duration string + PodName string + Selector string +} + +func (m PodStressMemoryExperiment) String() (string, error) { + tpl := ` +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: {{ .ExperimentName }} + namespace: {{ .Namespace }} +spec: + mode: {{ .Mode }} + {{- if .ModeValue }} + value: '{{ .ModeValue }}' + {{- end }} + duration: {{ .Duration }} + selector: + {{- if .Selector}} + labelSelectors: + {{ .Selector }} + {{- else}} + fieldSelectors: + metadata.name: {{ .PodName }} + {{- end}} + stressors: + memory: + workers: {{ .Workers }} + size: {{ .Memory }} +` + return MarshalTemplate( + m, + uuid.NewString(), + tpl, + ) +} + +type CRD struct { + Kind string `yaml:"kind"` + APIVersion string `yaml:"apiVersion"` + Metadata struct { + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + } `yaml:"metadata"` + Spec interface{} `yaml:"spec"` // Use interface{} if the spec can have various structures +} + +type NamedExperiment struct { + CRD + Name string + Path string + CRDBytes []byte +} + +func NewNamedExperiment(expPath string) (*NamedExperiment, error) { + data, err := os.ReadFile(expPath) + if err != nil { + return nil, err + } + + var exp CRD + err = yaml.Unmarshal(data, &exp) + if err != nil { + return nil, err + } + expName := exp.Metadata.Name + if expName == "" { + return nil, errors.Errorf("experiment metadata.name is empty") + } + + return &NamedExperiment{ + CRD: exp, + Name: expName, + Path: expPath, + CRDBytes: data, + }, nil +} + +func (m *Controller) readExistingExperimentTypes(dir string) ([]string, error) { + expTypes := make([]string, 0) + err := filepath.Walk( + dir, + func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.Name() != dir { + expTypes = append(expTypes, info.Name()) + return nil + } + return err + }) + if err != nil { + return nil, err + } + sort.Slice(expTypes, func(i, j int) bool { + return expTypes[i] < expTypes[j] + }) + L.Info().Strs("Order", expTypes).Msg("Order of experiment dirs execution") + return expTypes, nil +} + +func (m *Controller) ReadExperimentsFromDir(expTypes []string, dir string) ([]*NamedExperiment, error) { + expData := make([]*NamedExperiment, 0) + for _, expType := range expTypes { + targetDir := fmt.Sprintf("%s/%s", dir, expType) + if _, err := os.Stat(targetDir); err != nil { + // it's okay, some experiments may be skipped due configuration + continue + } + err := filepath.Walk( + fmt.Sprintf("%s/%s", dir, expType), + func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + exp, err := NewNamedExperiment(path) + if err != nil { + return err + } + expData = append(expData, exp) + return err + }) + if err != nil { + return nil, err + } + } + return expData, nil +} + +// maybeFailAll is a special case where we've labelled component properly but +// there is only one component, no need to apply experiment to part of a group, so we set 100% +func maybeFailAll(e lo.Entry[string, int], origValue string) string { + if e.Value == 1 { + return "100" + } + return origValue +} + +func (m *Controller) generate( + namespace string, + oapiSpecs []*OAPISpecData, + allPodsInfo map[string][]*PodResponse, + podsInfo []*PodResponse, + groupLabels []lo.Entry[string, int], + netLabels [][]string, +) (*ChaosSpecs, error) { + allExperimentsByType := make(map[string]map[string]string) + for _, expType := range m.cfg.Havoc.ExperimentTypes { + experiments := make(map[string]string) + switch expType { + case ChaosTypeHTTP: + for _, entry := range groupLabels { + if _, ok := m.cfg.Havoc.OpenAPI.Mapping[m.groupValueFromLabelSelector(entry.Key)]; ok { + if err := m.generateOAPIExperiments(experiments, namespace, entry, oapiSpecs); err != nil { + return nil, err + } + } + } + case ChaosTypeBlockchainSetHead: + for _, p := range allPodsInfo { + for _, pi := range p { + for _, nodeCfg := range m.cfg.Havoc.BlockchainRewindHead.NodesConfig { + if strings.Contains(pi.Metadata.Name, nodeCfg.ExecutorPodPrefix) { + for _, b := range nodeCfg.Blocks { + name := fmt.Sprintf("%s-%s-%d", ChaosTypeBlockchainSetHead, pi.Metadata.Name, b) + experiment, err := BlockchainRewindHeadExperiment{ + ExperimentName: name, + Metadata: &Metadata{Name: name}, + Namespace: namespace, + NodeInternalHTTPURL: nodeCfg.NodeInternalHTTPURL, + PodName: pi.Metadata.Name, + ExecutorContainerName: nodeCfg.ExecutorContainerName, + Blocks: b, + }.String() + if err != nil { + return nil, err + } + shortName := fmt.Sprintf("%s-%d", pi.Metadata.Name, b) + experiments[shortName] = experiment + } + } + } + } + } + case ChaosTypePartitionExternal: + if m.cfg.Havoc.ExternalTargets == nil { + continue + } + for _, u := range m.cfg.Havoc.ExternalTargets.URLs { + nsAndURLHash := fmt.Sprintf("%s-%s", namespace, urlHash(u)) + experiment, err := NetworkChaosExternalPartitionExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypePartitionExternal, nsAndURLHash), + Duration: m.cfg.Havoc.ExternalTargets.Duration, + ExternalURL: fmt.Sprintf("'%s'", u), + }.String() + if err != nil { + return nil, err + } + experiments[nsAndURLHash] = experiment + } + case ChaosTypePartitionGroup: + for _, pair := range netLabels { + for _, groupModeValue := range m.cfg.Havoc.NetworkPartition.GroupPercentage { + sanitizedLabel := sanitizeLabel(fmt.Sprintf("%s-to-%s", pair[0], pair[1])) + sanitizedLabel = fmt.Sprintf("%s-%s-perc", sanitizedLabel, groupModeValue) + experiment, err := NetworkChaosGroupPartitionExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypePartitionGroup, sanitizedLabel), + Duration: m.cfg.Havoc.NetworkPartition.Duration, + ModeFrom: "fixed-percent", + ModeFromValue: groupModeValue, + ModeTo: "fixed-percent", + ModeToValue: groupModeValue, + Direction: "from", + SelectorFrom: pair[0], + SelectorTo: pair[1], + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + for _, groupModeValue := range m.cfg.Havoc.NetworkPartition.GroupFixed { + sanitizedLabel := sanitizeLabel(fmt.Sprintf("%s-to-%s", pair[0], pair[1])) + sanitizedLabel = fmt.Sprintf("%s-%s-fixed", sanitizedLabel, groupModeValue) + experiment, err := NetworkChaosGroupPartitionExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypePartitionGroup, sanitizedLabel), + Duration: m.cfg.Havoc.NetworkPartition.Duration, + ModeFrom: "fixed-percent", + ModeFromValue: groupModeValue, + ModeTo: "fixed-percent", + ModeToValue: groupModeValue, + Direction: "from", + SelectorFrom: pair[0], + SelectorTo: pair[1], + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + } + case ChaosTypeFailure: + for _, pi := range podsInfo { + experiment, err := PodFailureExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeFailure, pi.Metadata.Name), + Mode: "one", + Duration: m.cfg.Havoc.Failure.Duration, + PodName: pi.Metadata.Name, + }.String() + if err != nil { + return nil, err + } + experiments[pi.Metadata.Name] = experiment + } + case ChaosTypeLatency: + for _, pi := range podsInfo { + experiment, err := NetworkChaosExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeLatency, pi.Metadata.Name), + Mode: "one", + Duration: m.cfg.Havoc.Latency.Duration, + Latency: m.cfg.Havoc.Latency.Latency, + PodName: pi.Metadata.Name, + }.String() + if err != nil { + return nil, err + } + experiments[pi.Metadata.Name] = experiment + } + case ChaosTypeStressCPU: + for _, pi := range podsInfo { + experiment, err := PodStressCPUExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeStressCPU, pi.Metadata.Name), + Duration: m.cfg.Havoc.StressCPU.Duration, + Workers: m.cfg.Havoc.StressCPU.Workers, + Load: m.cfg.Havoc.StressCPU.Load, + Mode: "one", + PodName: pi.Metadata.Name, + }.String() + if err != nil { + return nil, err + } + experiments[pi.Metadata.Name] = experiment + } + case ChaosTypeStressMemory: + for _, pi := range podsInfo { + experiment, err := PodStressMemoryExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeStressMemory, pi.Metadata.Name), + Duration: m.cfg.Havoc.StressMemory.Duration, + Workers: m.cfg.Havoc.StressMemory.Workers, + Memory: m.cfg.Havoc.StressMemory.Memory, + Mode: "one", + PodName: pi.Metadata.Name, + }.String() + if err != nil { + return nil, err + } + experiments[pi.Metadata.Name] = experiment + } + case ChaosTypeStressGroupMemory: + for _, entry := range groupLabels { + for _, groupModeValue := range m.cfg.Havoc.StressMemory.GroupPercentage { + groupModeValue = maybeFailAll(entry, groupModeValue) + sanitizedLabel := sanitizeLabel(entry.Key) + sanitizedLabel = fmt.Sprintf("%s-%s-perc", sanitizedLabel, groupModeValue) + experiment, err := PodStressMemoryExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeStressGroupMemory, sanitizedLabel), + Duration: m.cfg.Havoc.StressMemory.Duration, + Workers: m.cfg.Havoc.StressMemory.Workers, + Memory: m.cfg.Havoc.StressMemory.Memory, + Mode: "fixed-percent", + ModeValue: groupModeValue, + Selector: entry.Key, + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + for _, groupModeValue := range m.cfg.Havoc.StressMemory.GroupFixed { + groupModeValue = maybeFailAll(entry, groupModeValue) + sanitizedLabel := sanitizeLabel(entry.Key) + sanitizedLabel = fmt.Sprintf("%s-%s-fixed", sanitizedLabel, groupModeValue) + experiment, err := PodStressMemoryExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeStressGroupMemory, sanitizedLabel), + Duration: m.cfg.Havoc.StressMemory.Duration, + Workers: m.cfg.Havoc.StressMemory.Workers, + Memory: m.cfg.Havoc.StressMemory.Memory, + Mode: "fixed", + ModeValue: groupModeValue, + Selector: entry.Key, + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + } + case ChaosTypeStressGroupCPU: + for _, entry := range groupLabels { + for _, groupModeValue := range m.cfg.Havoc.StressCPU.GroupPercentage { + groupModeValue = maybeFailAll(entry, groupModeValue) + sanitizedLabel := sanitizeLabel(entry.Key) + sanitizedLabel = fmt.Sprintf("%s-%s-perc", sanitizedLabel, groupModeValue) + experiment, err := PodStressCPUExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeStressGroupCPU, sanitizedLabel), + Duration: m.cfg.Havoc.StressCPU.Duration, + Workers: m.cfg.Havoc.StressCPU.Workers, + Load: m.cfg.Havoc.StressCPU.Load, + Mode: "fixed-percent", + ModeValue: groupModeValue, + Selector: entry.Key, + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + for _, groupModeValue := range m.cfg.Havoc.StressCPU.GroupFixed { + groupModeValue = maybeFailAll(entry, groupModeValue) + sanitizedLabel := sanitizeLabel(entry.Key) + sanitizedLabel = fmt.Sprintf("%s-%s-fixed", sanitizedLabel, groupModeValue) + experiment, err := PodStressCPUExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeStressGroupCPU, sanitizedLabel), + Duration: m.cfg.Havoc.StressCPU.Duration, + Workers: m.cfg.Havoc.StressCPU.Workers, + Load: m.cfg.Havoc.StressCPU.Load, + Mode: "fixed", + ModeValue: groupModeValue, + Selector: entry.Key, + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + } + case ChaosTypeGroupFailure: + for _, entry := range groupLabels { + for _, groupModeValue := range m.cfg.Havoc.Failure.GroupPercentage { + groupModeValue = maybeFailAll(entry, groupModeValue) + sanitizedLabel := sanitizeLabel(entry.Key) + sanitizedLabel = fmt.Sprintf("%s-%s-perc", sanitizedLabel, groupModeValue) + experiment, err := PodFailureExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeGroupFailure, sanitizedLabel), + Duration: m.cfg.Havoc.Failure.Duration, + Mode: "fixed-percent", + ModeValue: groupModeValue, + Selector: entry.Key, + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + for _, groupModeValue := range m.cfg.Havoc.Failure.GroupFixed { + groupModeValue = maybeFailAll(entry, groupModeValue) + sanitizedLabel := sanitizeLabel(entry.Key) + sanitizedLabel = fmt.Sprintf("%s-%s-fixed", sanitizedLabel, groupModeValue) + experiment, err := PodFailureExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeGroupFailure, sanitizedLabel), + Duration: m.cfg.Havoc.Failure.Duration, + Mode: "fixed", + ModeValue: groupModeValue, + Selector: entry.Key, + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + } + case ChaosTypeGroupLatency: + for _, entry := range groupLabels { + for _, groupModeValue := range m.cfg.Havoc.Latency.GroupPercentage { + groupModeValue = maybeFailAll(entry, groupModeValue) + sanitizedLabel := sanitizeLabel(entry.Key) + sanitizedLabel = fmt.Sprintf("%s-%s-perc", sanitizedLabel, groupModeValue) + experiment, err := NetworkChaosExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeGroupLatency, sanitizedLabel), + Mode: "fixed-percent", + ModeValue: groupModeValue, + Duration: m.cfg.Havoc.Latency.Duration, + Latency: m.cfg.Havoc.Latency.Latency, + Selector: entry.Key, + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + for _, groupModeValue := range m.cfg.Havoc.Latency.GroupFixed { + groupModeValue = maybeFailAll(entry, groupModeValue) + sanitizedLabel := sanitizeLabel(entry.Key) + sanitizedLabel = fmt.Sprintf("%s-%s-fixed", sanitizedLabel, groupModeValue) + experiment, err := NetworkChaosExperiment{ + Namespace: namespace, + ExperimentName: fmt.Sprintf("%s-%s", ChaosTypeGroupLatency, sanitizedLabel), + Mode: "fixed", + ModeValue: groupModeValue, + Duration: m.cfg.Havoc.Latency.Duration, + Latency: m.cfg.Havoc.Latency.Latency, + Selector: entry.Key, + }.String() + if err != nil { + return nil, err + } + experiments[sanitizedLabel] = experiment + } + } + } + allExperimentsByType[expType] = experiments + } + return &ChaosSpecs{ + ExperimentsByType: allExperimentsByType, + }, nil +} + +func urlHash(url string) string { + hasher := md5.New() + hasher.Write([]byte(url)) + hashBytes := hasher.Sum(nil) + return hex.EncodeToString(hashBytes) +} + +func sanitizeLabel(label string) string { + sanitizedLabel := strings.Replace(label, "'", "", -1) + sanitizedLabel = strings.Replace(sanitizedLabel, ": ", "-", -1) + sanitizedLabel = strings.Replace(sanitizedLabel, ".", "-", -1) + sanitizedLabel = strings.Replace(sanitizedLabel, "/", "-", -1) + return sanitizedLabel +} + +type EventJSONItemResponse struct { + APIVersion string `json:"apiVersion"` + Count int `json:"count"` + EventTime any `json:"eventTime"` + FirstTimestamp time.Time `json:"firstTimestamp"` + InvolvedObject struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Name string `json:"name"` + Namespace string `json:"namespace"` + ResourceVersion string `json:"resourceVersion"` + UID string `json:"uid"` + } `json:"involvedObject"` + Kind string `json:"kind"` + LastTimestamp time.Time `json:"lastTimestamp"` + Message string `json:"message"` + Metadata struct { + Annotations struct { + ChaosMeshOrgType string `json:"chaos-mesh.org/type"` + } `json:"annotations"` + CreationTimestamp time.Time `json:"creationTimestamp"` + Name string `json:"name"` + Namespace string `json:"namespace"` + ResourceVersion string `json:"resourceVersion"` + UID string `json:"uid"` + } `json:"metadata"` + Reason string `json:"reason"` + ReportingComponent string `json:"reportingComponent"` + ReportingInstance string `json:"reportingInstance"` + Source struct { + Component string `json:"component"` + } `json:"source"` + Type string `json:"type"` +} + +type EventsJSONResponse struct { + APIVersion string `json:"apiVersion"` + Items []*EventJSONItemResponse `json:"items"` + Kind string `json:"kind"` + Metadata struct { + ResourceVersion string `json:"resourceVersion"` + } `json:"metadata"` +} + +func eventsForLastMinutes(out string, timeOfApplication time.Time) error { + var d *EventsJSONResponse + if err := json.Unmarshal([]byte(out), &d); err != nil { + return err + } + L.Debug().Msg("Listing all experiment events") + for _, i := range d.Items { + if i.LastTimestamp.After(timeOfApplication) { + L.Info(). + Time("Time", i.LastTimestamp). + Str("Reason", i.Reason). + Str("Message", i.Message). + Send() + } + } + return nil +} + +func (m *Controller) ApplyExperiment(exp *NamedExperiment, wait bool) error { + timeOfApplication := time.Now() + var errDefer error + if exp.Kind == ChaosTypeBlockchainSetHead { + return m.ApplyCustomKindChaosFile(exp, ChaosTypeBlockchainSetHead, wait) + } + L.Info(). + Str("Dir", m.cfg.Havoc.Dir). + Str("Type", exp.Kind). + Str("Name", exp.Metadata.Name). + Msg("Applying experiment manifest") + fmt.Println(string(exp.CRDBytes)) + _, err := ExecCmd(fmt.Sprintf("kubectl apply -f %s", exp.Path)) + if err != nil { + return errors.Wrap(err, ErrExperimentApply) + } + if wait { + resourceType := ExperimentTypesToCRDNames[exp.Kind] + if resourceType == "" { + return errors.Errorf("%s resource not present in %+v list", exp.Kind, ExperimentTypesToCRDNames) + } + _, err = ExecCmd( + fmt.Sprintf("kubectl wait -n %s %s --field-selector=metadata.name=%s --for condition=AllRecovered=True --timeout %s", + exp.Metadata.Namespace, + resourceType, + exp.Metadata.Name, + DefaultCMDTimeout, + )) + if err != nil { + return errors.Wrap(err, ErrExperimentTimeout) + } + out, err := ExecCmd( + fmt.Sprintf("kubectl get -n %s events --field-selector involvedObject.name=%s -o json", + exp.Metadata.Namespace, + exp.Name, + )) + if err != nil { + return err + } + if err = eventsForLastMinutes(out, timeOfApplication); err != nil { + return err + } + _, err = ExecCmd(fmt.Sprintf("kubectl -n %s delete %s %s", exp.Metadata.Namespace, resourceType, exp.Name)) + if err != nil { + return err + } + L.Info().Msg("Chaos experiment successfully recovered") + } + return errDefer +} + +type CurrentBlockResponse struct { + Result string `json:"result"` +} + +func (m *Controller) ApplyCustomKindChaosFile(exp *NamedExperiment, chaosType string, wait bool) error { + switch chaosType { + case ChaosTypeBlockchainSetHead: + var rewind *BlockchainRewindHeadExperiment + data, err := os.ReadFile(exp.Path) + if err != nil { + return nil + } + if err := yaml.Unmarshal(data, &rewind); err != nil { + return err + } + L.Info(). + Str("Dir", m.cfg.Havoc.Dir). + Str("Type", chaosType). + Str("Name", exp.Name). + Msg("Applying custom experiment") + fmt.Println(string(exp.CRDBytes)) + lastBlkCommand := fmt.Sprintf(`kubectl -n %s -it debug %s --image=%s --target=%s -- curl -s -X POST -H Content-Type:application/json --data {"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":88} %s`, + rewind.Namespace, + rewind.PodName, + DebugContainerImage, + rewind.ExecutorContainerName, + rewind.NodeInternalHTTPURL, + ) + out, err := ExecCmd(lastBlkCommand) + if err != nil { + return err + } + msg, err := findJSONMsg(out) + if err != nil { + return err + } + var res *CurrentBlockResponse + if err := json.Unmarshal([]byte(msg), &res); err != nil { + return err + } + decimalLastBlock, err := strconv.ParseInt(res.Result[2:], 16, 64) + if err != nil { + return err + } + moveToBlock := decimalLastBlock - rewind.Blocks + moveToBlockHex := strconv.FormatInt(moveToBlock, 16) + setHeadCommand := fmt.Sprintf(`kubectl -n %s -it debug %s --image=%s --target=%s -- curl -s -X POST -H Content-Type:application/json --data {"jsonrpc":"2.0","method":"debug_setHead","params":["0x%s"],"id":5} %s`, + rewind.Namespace, + rewind.PodName, + DebugContainerImage, + rewind.ExecutorContainerName, + moveToBlockHex, + rewind.NodeInternalHTTPURL, + ) + _, err = ExecCmd(setHeadCommand) + if err != nil { + return err + } + default: + return errors.New(ErrInvalidCustomKind) + } + return nil +} + +func findJSONMsg(s string) (string, error) { + startIndex := strings.Index(s, "{") + endIndex := strings.LastIndex(s, "}") + if startIndex != -1 && endIndex != -1 { + substring := s[startIndex : endIndex+1] + L.Debug(). + Str("Message", substring). + Msg("JSON substring response") + return substring, nil + } else { + return "", errors.New("no JSON substring found in response") + } +} + +// GenerateSpecs generates specs from namespace, should be used programmatically in tests +func (m *Controller) GenerateSpecs(ns string) error { + podsInfo, err := m.GetPodsInfo(ns) + if err != nil { + return err + } + _, _, err = m.generateSpecs(ns, podsInfo) + return err +} + +func (m *Controller) generateSpecs(namespace string, podListResponse *PodsListResponse) (*ChaosSpecs, []*PodResponse, error) { + L.Trace(). + Interface("PodListResponse", podListResponse). + Msg("Found pods") + all, noGroup, componentLabels, networkLabels, err := m.processPodInfoLo(podListResponse) + if err != nil { + return nil, nil, err + } + L.Info().Msg("Processing OpenAPI specs") + specs, err := m.ParseOpenAPISpecs() + if err != nil { + return nil, nil, err + } + L.Info().Msg("Generating chaos experiments") + csp, err := m.generate(namespace, specs, all, noGroup, componentLabels, networkLabels) + if err != nil { + return nil, nil, err + } + return csp, noGroup, csp.Dump(m.cfg.Havoc.Dir) +} diff --git a/havoc/go.mod b/havoc/go.mod new file mode 100644 index 000000000..edbbfd04f --- /dev/null +++ b/havoc/go.mod @@ -0,0 +1,96 @@ +module github.com/smartcontractkit/chainlink-testing-framework/havoc + +go 1.21 + +require ( + github.com/c-bata/go-prompt v0.2.6 + github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240821051457-da69c6d9617a + github.com/getkin/kin-openapi v0.122.0 + github.com/go-resty/resty/v2 v2.11.0 + github.com/google/uuid v1.5.0 + github.com/pelletier/go-toml/v2 v2.1.1 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.31.0 + github.com/samber/lo v1.39.0 + github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.1 + github.com/stretchr/testify v1.8.4 + github.com/urfave/cli/v2 v2.27.1 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.28.2 + k8s.io/client-go v0.28.2 +) + +require ( + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.10.1 // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/grafana/grafana-foundation-sdk/go v0.0.0-20240326122733-6f96a993222b // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/apiextensions-apiserver v0.28.1 // indirect + k8s.io/apimachinery v0.28.2 // indirect + k8s.io/component-base v0.28.2 // indirect + k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/invopop/yaml v0.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-tty v0.0.3 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pkg/term v1.2.0-beta.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.15.0 // indirect + sigs.k8s.io/controller-runtime v0.16.2 +) + +exclude github.com/chaos-mesh/chaos-mesh/api/v1alpha1 v0.0.0-20220226050744-799408773657 diff --git a/havoc/go.sum b/havoc/go.sum new file mode 100644 index 000000000..a6e130297 --- /dev/null +++ b/havoc/go.sum @@ -0,0 +1,309 @@ +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= +github.com/bxcodec/faker v2.0.1+incompatible/go.mod h1:BNzfpVdTwnFJ6GtfYTcQu6l6rHShT+veBxNCnjCx5XM= +github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= +github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240821051457-da69c6d9617a h1:6Pg3a6j/41QDzH/oYcMLwwKsf3x/HXcu9W/dBaf2Hzs= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20240821051457-da69c6d9617a/go.mod h1:x11iCbZV6hzzSQWMq610B6Wl5Lg1dhwqcVfeiWQQnQQ= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= +github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/getkin/kin-openapi v0.122.0 h1:WB9Jbl0Hp/T79/JF9xlSW5Kl9uYdk/AWD0yAd9HOM10= +github.com/getkin/kin-openapi v0.122.0/go.mod h1:PCWw/lfBrJY4HcdqE3jj+QFkaFK8ABoqo7PvqVhXXqw= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= +github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/grafana-foundation-sdk/go v0.0.0-20240326122733-6f96a993222b h1:Msqs1nc2qWMxTriDCITKl58Td+7Md/RURmUmH7RXKns= +github.com/grafana/grafana-foundation-sdk/go v0.0.0-20240326122733-6f96a993222b/go.mod h1:WtWosval1KCZP9BGa42b8aVoJmVXSg0EvQXi9LDSVZQ= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= +github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= +github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= +github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.1 h1:1/r1wQZ4TOFpZ13w94r7amdF096Z96RuEnkOmrz1BGE= +github.com/smartcontractkit/chainlink-testing-framework/grafana v0.0.1/go.mod h1:DC8sQMyTlI/44UCTL8QWFwb0bYNoXCfjwCv2hMivYZU= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= +k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= +k8s.io/apiextensions-apiserver v0.28.1 h1:l2ThkBRjrWpw4f24uq0Da2HaEgqJZ7pcgiEUTKSmQZw= +k8s.io/apiextensions-apiserver v0.28.1/go.mod h1:sVvrI+P4vxh2YBBcm8n2ThjNyzU4BQGilCQ/JAY5kGs= +k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= +k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= +k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY= +k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY= +k8s.io/component-base v0.28.2 h1:Yc1yU+6AQSlpJZyvehm/NkJBII72rzlEsd6MkBQ+G0E= +k8s.io/component-base v0.28.2/go.mod h1:4IuQPQviQCg3du4si8GpMrhAIegxpsgPngPRR/zWpzc= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.16.2 h1:mwXAVuEk3EQf478PQwQ48zGOXvW27UJc8NHktQVuIPU= +sigs.k8s.io/controller-runtime v0.16.2/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/havoc/havoc.go b/havoc/havoc.go new file mode 100644 index 000000000..e279e391a --- /dev/null +++ b/havoc/havoc.go @@ -0,0 +1,80 @@ +package havoc + +import ( + "fmt" + "os" + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +const ( + ChaosTypeBlockchainSetHead = "blockchain_rewind_head" + ChaosTypeFailure = "failure" + ChaosTypeGroupFailure = "group-failure" + ChaosTypeLatency = "latency" + ChaosTypeGroupLatency = "group-latency" + ChaosTypeStressMemory = "memory" + ChaosTypeStressGroupMemory = "group-memory" + ChaosTypeStressCPU = "cpu" + ChaosTypeStressGroupCPU = "group-cpu" + ChaosTypePartitionExternal = "external" + ChaosTypePartitionGroup = "group-partition" + ChaosTypeHTTP = "http" +) + +var ( + ExperimentTypesToCRDNames = map[string]string{ + "PodChaos": "podchaos.chaos-mesh.org", + "StressChaos": "stresschaos.chaos-mesh.org", + "NetworkChaos": "networkchaos.chaos-mesh.org", + "HTTPChaos": "httpchaos.chaos-mesh.org", + } +) + +var L zerolog.Logger + +func SetGlobalLogger(l zerolog.Logger) { + L = l.With().Str("Component", "havoc").Logger() +} + +func InitDefaultLogging() { + lvl, err := zerolog.ParseLevel(os.Getenv("HAVOC_LOG_LEVEL")) + if err != nil { + panic(err) + } + if lvl.String() == "" { + lvl = zerolog.InfoLevel + } + L = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(lvl) +} + +type ChaosSpecs struct { + ExperimentsByType map[string]map[string]string +} + +func (m *ChaosSpecs) Dump(dir string) error { + if err := os.RemoveAll(dir); err != nil { + return err + } + if err := os.Mkdir(dir, os.ModePerm); err != nil { + return err + } + L.Info().Str("Dir", dir).Msg("Writing experiments to a dir") + for expType := range m.ExperimentsByType { + if len(m.ExperimentsByType[expType]) == 0 { + continue + } + if err := os.Mkdir(fmt.Sprintf("%s/%s", dir, expType), os.ModePerm); err != nil { + return err + } + for expName, expBody := range m.ExperimentsByType[expType] { + fname := strings.ToLower(fmt.Sprintf("%s/%s/%s-%s.yaml", dir, expType, expType, expName)) + if err := os.WriteFile(fname, []byte(expBody), os.ModePerm); err != nil { + return err + } + } + } + return nil +} diff --git a/havoc/havoc.toml b/havoc/havoc.toml new file mode 100644 index 000000000..7e9b86695 --- /dev/null +++ b/havoc/havoc.toml @@ -0,0 +1,141 @@ +[havoc] +# dir is a custom dir you can select, if null monkey will create a new dir +dir = "experiments-crib-core" +# if you have multiple products inside one namespace this can help to filter by label in k=v format +namespace_label_filter = "" +# pods with this prefix will be ignored when generating experiments +ignore_pods = ["-db-"] +# name of the key to select components in the namespace +component_label_key = "havoc-component-group" +# group labels containing these strings will be ignored when generating group experiments +ignore_group_labels = [ + "mainnet", + "release", + "intents.otterize.com", + "pod-template-hash", + "rollouts-pod-template-hash", + "chain.link/app", + "chain.link/cost-center", + "chain.link/env", + "chain.link/project", + "chain.link/team", + "app.kubernetes.io/part-of", + "app.kubernetes.io/managed-by", + "app.chain.link/product", + "app.kubernetes.io/version", + "app.chain.link/blockchain", + "app.kubernetes.io/instance", + "app.kubernetes.io/name", +] +# these are experiment types you'd like to generate +experiment_types = [ + "external", + "failure", + "latency", + "cpu", + "memory", + "group-failure", + "group-latency", + "group-cpu", + "group-memory", + "group-partition", + "blockchain_rewind_head", + "http" +] + +[havoc.failure] +# duration of a "failure" experiment +duration = "10s" +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_fixed = ["3", "2", "1"] + +[havoc.latency] +# duration of "latency" experiment +duration = "10s" +# constant latency to inject +latency = "300ms" +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_fixed = ["3", "2", "1"] + +[havoc.stress_memory] +# duration of "stress" experiment affecting pod memory +duration = "10s" +# amount of workers which occupies memory +workers = 1 +# total amount of memory occupied +memory = "512MB" +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_fixed = ["3", "2", "1"] + +[havoc.stress_cpu] +# duration of "stress" experiment affecting pod CPU +duration = "10s" +# amount of workers which occupies cpu +workers = 1 +# amount of CPU core utilization, 100 means 1 worker will consume 1 cpu, 2 workers + 100 load = 2 CPUs +load = 100 +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_fixed = ["3", "2", "1"] + +[havoc.network_partition] +# duration of "network partition" experiment affecting pod CPU +duration = "30s" +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_percentage = ["100"] +# a label to split pods for experiments +label = "havoc-network-group" + +[havoc.blockchain_rewind_head] +# duration of "blockchain" experiment +duration = "30s" + +[[havoc.blockchain_rewind_head.nodes]] +# label of executor pod +executor_pod_prefix = "geth-1337" +# executor container name +executor_container_name = "geth-network" +# blockchain node internal HTTP URL +node_internal_http_url = "geth-1337:8544" +# blocks to rewind from last +blocks = [30, 20, 10] + +[[havoc.blockchain_rewind_head.nodes]] +# label of executor pod +executor_pod_prefix = "geth-2337" +# executor container name +executor_container_name = "geth-network" +# blockchain node internal HTTP URL +node_internal_http_url = "geth-2337:8544" +# blocks to rewind from last +blocks = [30, 20, 10] + +[havoc.external_targets] +# duration of "external" experiment +duration = "10s" +# URL of external service that'd fail to resolve +urls = ["www.google.com"] + +# you can map OpenAPI 3.0.0 specifications to your component groups, let's say you have +# component_label_key = "havoc-component-group" and some pods having "havoc-component-group: node" +[havoc.openapi] +[havoc.openapi.mapping.node] +[[havoc.openapi.mapping.node.spec_to_port]] +# port on which your instances are exposing this API +port = 8080 +# path to OpenAPI 3.0.0 +path = "testdata/openapi_specs/petshop.yaml" + +[havoc.monkey] +# havoc monkey mode: +# seq - runs all experiments from all dirs sequentially one time +# rand - runs random experiments from all dirs +mode = "rand" +# duration of havoc monkey +duration = "3m" +# cooldown between experiments +cooldown = "5s" + +[havoc.grafana] +# UIDs of dashboard which should be annotated with chaos experiments metadata +# You can also try to use name as you see it in the top bar of your dashboard but that's not guaranteed to match +dashboard_uids = ["WaspDebug", "e98b5451-12dc-4a8b-9576-2c0b67ddbd0c"] diff --git a/havoc/havoc_test.go b/havoc/havoc_test.go new file mode 100644 index 000000000..2a55bbbf6 --- /dev/null +++ b/havoc/havoc_test.go @@ -0,0 +1,134 @@ +package havoc + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + // We are not testing with real k8s, namespace is just a placeholder that should match in snapshots/results + Namespace = "cl-cluster" + TestDataDir = "testdata" + SnapshotDir = filepath.Join(TestDataDir, "snapshot") + ResultsDir = filepath.Join(TestDataDir, "results") + DeploymentsDir = filepath.Join(TestDataDir, "deployments") + ConfigsDir = filepath.Join(TestDataDir, "configs") + OAPISpecs = filepath.Join(TestDataDir, "openapi_specs") +) + +var ( + AllExperimentTypes = []string{ + ChaosTypeFailure, + ChaosTypeLatency, + ChaosTypeGroupFailure, + ChaosTypeGroupLatency, + ChaosTypeStressMemory, + ChaosTypeStressGroupMemory, + ChaosTypeStressCPU, + ChaosTypeStressGroupCPU, + ChaosTypePartitionGroup, + ChaosTypeHTTP, + ChaosTypePartitionExternal, + ChaosTypeBlockchainSetHead, + } +) + +func init() { + InitDefaultLogging() +} + +func setup(t *testing.T, podsInfoPath string, configPath string, resultsDir string) (*Controller, *PodsListResponse) { + d, err := os.ReadFile(filepath.Join(DeploymentsDir, podsInfoPath)) + require.NoError(t, err) + var plr *PodsListResponse + err = json.Unmarshal(d, &plr) + require.NoError(t, err) + var cfg *Config + if configPath != "" { + cfg, err = ReadConfig(filepath.Join(ConfigsDir, configPath)) + require.NoError(t, err) + } else { + cfg = DefaultConfig() + cfg.Havoc.Dir = filepath.Join(ResultsDir, resultsDir) + } + m, err := NewController(cfg) + require.NoError(t, err) + return m, plr +} + +func TestSmokeParsingGenerating(t *testing.T) { + type test struct { + name string + podsDumpName string + configName string + snapshotDir string + resultsDir string + } + tests := []test{ + { + name: "can generate for 1 pod without groups", + podsDumpName: "deployment_single_pod.json", + configName: "", + snapshotDir: "single_pod", + resultsDir: "single_pod", + }, + { + name: "can generate for a component group", + podsDumpName: "deployment_single_group.json", + configName: "", + snapshotDir: "single_group", + resultsDir: "single_group", + }, + { + name: "standalone pods + component group + network group + blockchain experiments", + podsDumpName: "deployment_crib_block_rewind.json", + configName: "crib-all.toml", + snapshotDir: "all", + resultsDir: "all", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + m, plr := setup(t, tc.podsDumpName, tc.configName, tc.resultsDir) + _, _, err := m.generateSpecs(Namespace, plr) + require.NoError(t, err) + snapshotData, err := m.ReadExperimentsFromDir(AllExperimentTypes, filepath.Join(SnapshotDir, tc.snapshotDir)) + require.NoError(t, err) + generatedData, err := m.ReadExperimentsFromDir(AllExperimentTypes, filepath.Join(ResultsDir, tc.resultsDir)) + require.NoError(t, err) + require.Equal(t, len(snapshotData), len(generatedData)) + for i := range snapshotData { + // Replace snapshot dir name to match it with expected results path + snapshotData[i].Path = strings.ReplaceAll(snapshotData[i].Path, SnapshotDir, ResultsDir) + require.Equal(t, snapshotData[i], generatedData[i]) + } + }) + } +} + +/* +These are just an easy way to enter debug with arbitrary config, or some tweaks, run it manually +*/ +func TestManualGenerate(t *testing.T) { + cfg, err := ReadConfig("havoc.toml") + require.NoError(t, err) + m, err := NewController(cfg) + require.NoError(t, err) + err = m.GenerateSpecs("cl-cluster") + require.NoError(t, err) +} + +func TestManualRun(t *testing.T) { + cfg, err := ReadConfig("havoc.toml") + require.NoError(t, err) + m, err := NewController(cfg) + require.NoError(t, err) + err = m.Run() + require.NoError(t, err) +} diff --git a/havoc/img.png b/havoc/img.png new file mode 100644 index 0000000000000000000000000000000000000000..094d857ed845aed383369c70f874ede64f50e2fd GIT binary patch literal 223660 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 literal 0 HcmV?d00001 diff --git a/havoc/k8schaos/README.md b/havoc/k8schaos/README.md new file mode 100644 index 000000000..78d0acecd --- /dev/null +++ b/havoc/k8schaos/README.md @@ -0,0 +1,211 @@ +## k8schaos + +The `k8schaos` package is a Go library designed to facilitate chaos testing within Kubernetes environments using Chaos Mesh. It offers a structured way to define, execute, and manage chaos experiments as code, directly integrated into Go applications or testing suites. This package simplifies the creation and control of Chaos Mesh experiments, including network chaos, pod failures, and stress testing on Kubernetes clusters. + +### Features + +- **Chaos Object Management:** Easily create, update, pause, resume, and delete chaos experiments using Go structures and methods. +- **Lifecycle Hooks:** Utilize chaos listeners to hook into lifecycle events of chaos experiments, such as creation, start, pause, resume, and finish. +- **Support for Various Chaos Experiments:** Create and manage different types of chaos experiments like NetworkChaos, IOChaos, StressChaos, PodChaos, and HTTPChaos. +- **Chaos Experiment Status Monitoring:** Monitor and react to the status of chaos experiments programmatically. + +### Installation + +To use k8schaos in your project, ensure you have a Go environment setup. Then, install the package using go get: + +``` +go get -u github.com/smartcontractkit/havoc/k8schaos +``` + +Ensure your Kubernetes cluster is accessible and that you have Chaos Mesh installed and configured. + +### Monitoring and Observability in Chaos Experiments + +`k8schaos` enhances chaos experiment observability through structured logging and Grafana annotations, facilitated by implementing the ChaosListener interface. This approach allows for detailed monitoring, debugging, and visual representation of chaos experiments' impact. + +#### Structured Logging with ChaosLogger + +`ChaosLogger` leverages the zerolog library to provide structured, queryable logging of chaos events. It automatically logs key lifecycle events such as creation, start, pause, and termination of chaos experiments, including detailed contextual information. + +Instantiate `ChaosLogger` and register it as a listener to your chaos experiments: + +``` +logger := k8schaos.NewChaosLogger() +chaos.AddListener(logger) +``` + +### Default package logger + +k8schaos/logger.go contains default `Logger` instance for the package. + +#### Visual Monitoring with Grafana Annotations + +`SingleLineGrafanaAnnotator` is a `ChaosListener` that annotates Grafana dashboards with chaos experiment events. This visual representation helps correlate chaos events with their effects on system metrics and logs. + +Initialize `SingleLineGrafanaAnnotator` with your Grafana instance details and register it alongside `ChaosLogger`: + +``` +annotator := k8schaos.NewSingleLineGrafanaAnnotator( + "http://grafana-instance.com", + "grafana-access-token", + "dashboard-uid", +) +chaos.AddListener(annotator) +``` + +### Creating a Chaos Experiment + +To create a chaos experiment, define the chaos object options, initialize a chaos experiment with NewChaos, and then call Create to start the experiment. + +Here is an example of creating and starting a PodChaos experiment: + +``` +package main + +import ( + "context" + "github.com/smartcontractkit/havoc/k8schaos" + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +func main() { + // Initialize dependencies + client, err := k8schaos.NewChaosMeshClient() + if err != nil { + panic(err) + } + logger := k8schaos.NewChaosLogger() + annotator := k8schaos.NewSingleLineGrafanaAnnotator( + "http://grafana-instance.com", + "grafana-access-token", + "dashboard-uid", + ) + + // Define chaos experiment + podChaos := &v1alpha1.PodChaos{ /* PodChaos spec */ } + chaos, err := k8schaos.NewChaos(k8schaos.ChaosOpts{ + Object: podChaos, + Description: "Pod failure example", + DelayCreate: 5 * time.Second, + Client: client, + }) + if err != nil { + panic(err) + } + + // Register listeners + chaos.AddListener(logger) + chaos.AddListener(annotator) + + // Start chaos experiment + chaos.Create(context.Background()) + + // Manage chaos lifecycle... +} +``` + +### Test Example + +``` +func TestChaosDON(t *testing.T) { + testDuration := time.Minute * 60 + + // Load test config + cfg := &config.MercuryQAEnvChaos{} + + // Define chaos experiments and their schedule + + k8sClient, err := havoc.NewChaosMeshClient() + require.NoError(t, err) + + // Test 3.2: Disable 2 nodes simultaneously + + podFailureChaos4, err := k8s_chaos.MercuryPodChaosSchedule(k8s_chaos.MercuryScheduledPodChaosOpts{ + Name: "schedule-don-ocr-node-failure-4", + Description: "Disable 2 nodes (clc-ocr-mercury-arb-testnet-qa-nodes-3 and clc-ocr-mercury-arb-testnet-qa-nodes-4)", + DelayCreate: time.Minute * 0, + Duration: time.Minute * 20, + Namespace: cfg.ChaosNodeNamespace, + PodSelector: v1alpha1.PodSelector{ + Mode: v1alpha1.AllMode, + Selector: v1alpha1.PodSelectorSpec{ + GenericSelectorSpec: v1alpha1.GenericSelectorSpec{ + Namespaces: []string{cfg.ChaosNodeNamespace}, + ExpressionSelectors: v1alpha1.LabelSelectorRequirements{ + { + Key: "app.kubernetes.io/instance", + Operator: "In", + Values: []string{ + "clc-ocr-mercury-arb-testnet-qa-nodes-3", + "clc-ocr-mercury-arb-testnet-qa-nodes-4", + }, + }, + }, + }, + }, + }, + Client: k8sClient, + }) + require.NoError(t, err) + + // Test 3.3: Disable 3 nodes simultaneously + + podFailureChaos5, err := k8s_chaos.MercuryPodChaosSchedule(k8s_chaos.MercuryScheduledPodChaosOpts{ + Name: "schedule-don-ocr-node-failure-5", + Description: "Disable 3 nodes (clc-ocr-mercury-arb-testnet-qa-nodes-3, clc-ocr-mercury-arb-testnet-qa-nodes-4 and clc-ocr-mercury-arb-testnet-qa-nodes-5)", + DelayCreate: time.Minute * 40, + Duration: time.Minute * 20, + Namespace: cfg.ChaosNodeNamespace, + PodSelector: v1alpha1.PodSelector{ + Mode: v1alpha1.AllMode, + Selector: v1alpha1.PodSelectorSpec{ + GenericSelectorSpec: v1alpha1.GenericSelectorSpec{ + Namespaces: []string{cfg.ChaosNodeNamespace}, + ExpressionSelectors: v1alpha1.LabelSelectorRequirements{ + { + Key: "app.kubernetes.io/instance", + Operator: "In", + Values: []string{ + "clc-ocr-mercury-arb-testnet-qa-nodes-3", + "clc-ocr-mercury-arb-testnet-qa-nodes-4", + "clc-ocr-mercury-arb-testnet-qa-nodes-5", + }, + }, + }, + }, + }, + }, + Client: k8sClient, + }) + require.NoError(t, err) + + chaosList := []havoc.ChaosEntity{ + podFailureChaos4, + podFailureChaos5, + } + + for _, chaos := range chaosList { + chaos.AddListener(havoc.NewChaosLogger()) + chaos.AddListener(havoc.NewSingleLineGrafanaAnnotator(cfg.GrafanaURL, cfg.GrafanaToken, cfg.GrafanaDashboardUID)) + + // Fail the test if the chaos object already exists + exists, err := havoc.ChaosObjectExists(chaos.GetObject(), k8sClient) + require.NoError(t, err) + require.False(t, exists, "chaos object already exists: %s. Delete it before starting the test", chaos.GetChaosName()) + + chaos.Create(context.Background()) + } + + t.Cleanup(func() { + for _, chaos := range chaosList { + // Delete chaos object if it still exists + chaos.Delete(context.Background()) + } + }) + + // Simulate user activity/load for the duration of the chaos experiments + runUserLoad(t, cfg, testDuration) +} +``` diff --git a/havoc/k8schaos/chaos.go b/havoc/k8schaos/chaos.go new file mode 100644 index 000000000..7f3f15a03 --- /dev/null +++ b/havoc/k8schaos/chaos.go @@ -0,0 +1,615 @@ +package k8schaos + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/pkg/errors" + "github.com/rs/zerolog" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Chaos struct { + Object client.Object + Description string + DelayCreate time.Duration // Delay before creating the chaos object + Status ChaosStatus + Client client.Client + listeners []ChaosListener + cancelMonitor context.CancelFunc + startTime time.Time + endTime time.Time + logger *zerolog.Logger +} + +// ChaosStatus represents the status of a chaos experiment. +type ChaosStatus string + +// These constants define possible states of a chaos experiment. +const ( + StatusCreated ChaosStatus = "created" + StatusCreationFailed ChaosStatus = "creation_failed" + StatusRunning ChaosStatus = "running" + StatusPaused ChaosStatus = "paused" + StatusFinished ChaosStatus = "finished" + StatusDeleted ChaosStatus = "deleted" + StatusUnknown ChaosStatus = "unknown" // For any state that doesn't match the above +) + +type ChaosOpts struct { + Object client.Object + Description string + DelayCreate time.Duration + Client client.Client + Listeners []ChaosListener + Logger *zerolog.Logger +} + +func NewChaos(opts ChaosOpts) (*Chaos, error) { + if opts.Client == nil { + return nil, errors.New("client is required") + } + if opts.Object == nil { + return nil, errors.New("chaos object is required") + } + if opts.Logger == nil { + return nil, errors.New("logger is required") + } + + return &Chaos{ + Object: opts.Object, + Description: opts.Description, + DelayCreate: opts.DelayCreate, + Client: opts.Client, + listeners: opts.Listeners, + logger: opts.Logger, + }, nil +} + +// Create initiates a delayed creation of a chaos object, respecting context cancellation and deletion requests. +// It uses a timer based on `DelayCreate` and calls `create` method upon expiration unless preempted by deletion. +func (c *Chaos) Create(ctx context.Context) { + done := make(chan struct{}) + + // Create the timer with the delay to create the chaos object + timer := time.NewTimer(c.DelayCreate) + + go func() { + select { + case <-ctx.Done(): + // If the context is canceled, stop the timer and exit + if !timer.Stop() { + <-timer.C // If the timer already expired, drain the channel + } + close(done) // Signal that the operation was canceled + case <-timer.C: + // Timer expired, check if deletion was not requested + if c.Status != StatusDeleted { + c.createNow(ctx) + } + close(done) // Signal that the creation process is either done or skipped + } + }() +} + +func (c *Chaos) Update(ctx context.Context) error { + // Modify the resource + // For example, adding or updating an annotation + annotations := c.Object.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations["example.com/trigger-injection"] = "true" + c.Object.SetAnnotations(annotations) + + if err := c.Client.Update(ctx, c.Object); err != nil { + return errors.Wrap(err, "failed to update chaos object") + } + + return nil +} + +// createNow is a private method that encapsulates the chaos object creation logic. +func (c *Chaos) createNow(ctx context.Context) { + if err := c.Client.Create(ctx, c.Object); err != nil { + c.notifyListeners(string(StatusCreationFailed), err) + return + } + c.notifyListeners(string(StatusCreated), nil) + + // Create a cancellable context for monitorStatus + monitorCtx, cancel := context.WithCancel(ctx) + c.cancelMonitor = cancel + go c.monitorStatus(monitorCtx) +} + +func (c *Chaos) Pause(ctx context.Context) error { + err := c.updateChaosObject(ctx) + if err != nil { + return errors.Wrap(err, "could not update the chaos object") + } + + annotations := c.Object.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + annotations[v1alpha1.PauseAnnotationKey] = strconv.FormatBool(true) + c.Object.SetAnnotations(annotations) + + err = c.Client.Update(context.Background(), c.Object) + if err != nil { + return errors.Wrap(err, "could not update the annotation to set the chaos experiment into pause state") + } + + c.notifyListeners("paused", nil) + return nil +} + +func (c *Chaos) Resume(ctx context.Context) error { + // Implement resume logic here + c.notifyListeners("resumed", nil) + return nil +} + +func (c *Chaos) Delete(ctx context.Context) error { + // Cancel the monitoring goroutine + if c.cancelMonitor != nil { + c.cancelMonitor() + } + + // If the chaos was running or paused, update the status and notify listeners + if c.Status == StatusPaused || c.Status == StatusRunning { + err := c.updateChaosObject(ctx) + if err != nil { + return errors.Wrap(err, "could not update the chaos object") + } + c.Status = StatusFinished + c.endTime = time.Now() + c.notifyListeners("finished", nil) + } + + if err := c.Client.Delete(ctx, c.Object); err != nil { + return errors.Wrap(err, "failed to delete chaos object") + } + + c.Status = StatusDeleted + + c.logger.Info().Str("name", c.GetChaosName()).Msg("Chaos deleted") + + return nil +} + +func (c *Chaos) GetObject() client.Object { + return c.Object +} + +func (c *Chaos) GetChaosName() string { + return c.Object.GetName() +} + +func (c *Chaos) GetChaosDescription() string { + return c.Description +} + +func (c *Chaos) GetChaosTypeStr() string { + switch c.Object.(type) { + case *v1alpha1.NetworkChaos: + return "NetworkChaos" + case *v1alpha1.IOChaos: + return "IOChaos" + case *v1alpha1.StressChaos: + return "StressChaos" + case *v1alpha1.PodChaos: + return "PodChaos" + case *v1alpha1.HTTPChaos: + return "HTTPChaos" + default: + return "Unknown" + } +} + +func (c *Chaos) GetChaosSpec() interface{} { + switch spec := c.Object.(type) { + case *v1alpha1.NetworkChaos: + return spec.Spec + case *v1alpha1.IOChaos: + return spec.Spec + case *v1alpha1.StressChaos: + return spec.Spec + case *v1alpha1.PodChaos: + return spec.Spec + case *v1alpha1.HTTPChaos: + return spec.Spec + default: + return nil + } +} + +func (c *Chaos) GetChaosDuration() (time.Duration, error) { + var durationStr *string + switch spec := c.Object.(type) { + case *v1alpha1.NetworkChaos: + durationStr = spec.Spec.Duration + case *v1alpha1.IOChaos: + durationStr = spec.Spec.Duration + case *v1alpha1.StressChaos: + durationStr = spec.Spec.Duration + case *v1alpha1.PodChaos: + durationStr = spec.Spec.Duration + case *v1alpha1.HTTPChaos: + durationStr = spec.Spec.Duration + } + + if durationStr == nil { + return time.Duration(0), fmt.Errorf("could not get duration for chaos object: %v", c.Object) + } + duration, err := time.ParseDuration(*durationStr) + if err != nil { + return time.Duration(0), fmt.Errorf("could not parse duration: %w", err) + } + return duration, nil +} + +func (c *Chaos) GetChaosEvents() (*corev1.EventList, error) { + listOpts := []client.ListOption{ + client.InNamespace(c.Object.GetNamespace()), + client.MatchingFields{"involvedObject.name": c.Object.GetName(), "involvedObject.kind": c.GetChaosKind()}, + } + events := &corev1.EventList{} + if err := c.Client.List(context.Background(), events, listOpts...); err != nil { + return nil, fmt.Errorf("could not list chaos events: %w", err) + } + + return events, nil +} + +func (c *Chaos) GetChaosKind() string { + switch c.Object.(type) { + case *v1alpha1.NetworkChaos: + return "NetworkChaos" + case *v1alpha1.IOChaos: + return "IOChaos" + case *v1alpha1.StressChaos: + return "StressChaos" + case *v1alpha1.PodChaos: + return "PodChaos" + case *v1alpha1.HTTPChaos: + return "HTTPChaos" + default: + panic(fmt.Sprintf("could not get chaos kind for object: %v", c.Object)) + } +} + +func (c *Chaos) GetChaosStatus() (*v1alpha1.ChaosStatus, error) { + switch obj := c.Object.(type) { + case *v1alpha1.NetworkChaos: + return obj.GetStatus(), nil + case *v1alpha1.IOChaos: + return obj.GetStatus(), nil + case *v1alpha1.StressChaos: + return obj.GetStatus(), nil + case *v1alpha1.PodChaos: + return obj.GetStatus(), nil + case *v1alpha1.HTTPChaos: + return obj.GetStatus(), nil + default: + return nil, fmt.Errorf("could not get chaos status for %s", c.GetChaosKind()) + } +} + +func (c *Chaos) GetExperimentStatus() (v1alpha1.ExperimentStatus, error) { + switch obj := c.Object.(type) { + case *v1alpha1.NetworkChaos: + return obj.Status.Experiment, nil + case *v1alpha1.IOChaos: + return obj.Status.Experiment, nil + case *v1alpha1.StressChaos: + return obj.Status.Experiment, nil + case *v1alpha1.PodChaos: + return obj.Status.Experiment, nil + case *v1alpha1.HTTPChaos: + return obj.Status.Experiment, nil + default: + return v1alpha1.ExperimentStatus{}, fmt.Errorf("could not experiment status for object: %v", c.Object) + } +} + +func ChaosObjectExists(object client.Object, c client.Client) (bool, error) { + switch obj := object.(type) { + case *v1alpha1.NetworkChaos, *v1alpha1.IOChaos, *v1alpha1.StressChaos, *v1alpha1.PodChaos, *v1alpha1.HTTPChaos, *v1alpha1.Schedule: + err := c.Get(context.Background(), client.ObjectKeyFromObject(obj), obj) + if err != nil { + if client.IgnoreNotFound(err) == nil { + // If the error is NotFound, the object does not exist. + return false, nil + } + // For any other errors, return the error. + return false, err + } + // If there's no error, the object exists. + return true, nil + default: + return false, fmt.Errorf("unsupported chaos object type: %T", obj) + } +} + +func (c *Chaos) updateChaosObject(ctx context.Context) error { + switch obj := c.Object.(type) { + case *v1alpha1.NetworkChaos: + var objOut = &v1alpha1.NetworkChaos{} + err := c.Client.Get(ctx, client.ObjectKeyFromObject(obj), objOut) + if err != nil { + return errors.Wrap(err, "could not get network chaos object") + } + c.Object = objOut + case *v1alpha1.IOChaos: + var objOut = &v1alpha1.IOChaos{} + err := c.Client.Get(ctx, client.ObjectKeyFromObject(obj), objOut) + if err != nil { + return errors.Wrap(err, "could not get IO chaos object") + } + c.Object = objOut + case *v1alpha1.StressChaos: + var objOut = &v1alpha1.StressChaos{} + err := c.Client.Get(ctx, client.ObjectKeyFromObject(obj), objOut) + if err != nil { + return errors.Wrap(err, "could not get stress chaos object") + } + c.Object = objOut + case *v1alpha1.PodChaos: + var objOut = &v1alpha1.PodChaos{} + err := c.Client.Get(ctx, client.ObjectKeyFromObject(obj), objOut) + if err != nil { + return errors.Wrap(err, "could not get pod chaos object") + } + c.Object = objOut + case *v1alpha1.HTTPChaos: + var objOut = &v1alpha1.HTTPChaos{} + err := c.Client.Get(ctx, client.ObjectKeyFromObject(obj), objOut) + if err != nil { + return errors.Wrap(err, "could not get HTTP chaos object") + } + c.Object = objOut + case *v1alpha1.Schedule: + var objOut = &v1alpha1.Schedule{} + err := c.Client.Get(ctx, client.ObjectKeyFromObject(obj), objOut) + if err != nil { + return errors.Wrap(err, "could not get schedule object") + } + c.Object = objOut + default: + return fmt.Errorf("unsupported chaos object type: %T", obj) + } + + return nil +} + +func isConditionTrue(status *v1alpha1.ChaosStatus, expectedCondition v1alpha1.ChaosCondition) bool { + if status == nil { + return false + } + + for _, condition := range status.Conditions { + if condition.Type == expectedCondition.Type { + return condition.Status == expectedCondition.Status + } + } + return false +} + +func (c *Chaos) AddListener(listener ChaosListener) { + c.listeners = append(c.listeners, listener) +} + +// GetStartTime returns the time when the chaos experiment started +func (c *Chaos) GetStartTime() time.Time { + return c.startTime +} + +// GetEndTime returns the time when the chaos experiment ended +func (c *Chaos) GetEndTime() time.Time { + return c.endTime +} + +// GetExpectedEndTime returns the time when the chaos experiment is expected to end +func (c *Chaos) GetExpectedEndTime() (time.Time, error) { + duration, err := c.GetChaosDuration() + if err != nil { + return time.Time{}, err + } + return c.startTime.Add(duration), nil +} + +type ChaosEventDetails struct { + Event string + Chaos *Chaos + Error error +} + +func (c *Chaos) notifyListeners(event string, err error) { + for _, listener := range c.listeners { + switch event { + case "created": + listener.OnChaosCreated(*c) + case string(StatusCreationFailed): + listener.OnChaosCreationFailed(*c, err) + case "started": + listener.OnChaosStarted(*c) + case "paused": + listener.OnChaosPaused(*c) + case "resumed": + listener.OnChaosStarted(*c) // Assuming "resumed" triggers "started" + case "finished": + listener.OnChaosEnded(*c) + case "unknown": + listener.OnChaosStatusUnknown(*c) + } + } +} + +func (c *Chaos) monitorStatus(ctx context.Context) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + err := c.updateChaosObject(context.Background()) + if err != nil { + c.logger.Error().Err(err).Msg("failed to update chaos object") + continue + } + chaosStatus, err := c.GetChaosStatus() + if err != nil { + c.logger.Error().Err(err).Msg("failed to get chaos status") + continue + } + + var currentStatus ChaosStatus + + allRecovered := v1alpha1.ChaosCondition{ + Type: v1alpha1.ConditionAllRecovered, + Status: corev1.ConditionTrue, + } + allInjected := v1alpha1.ChaosCondition{ + Type: v1alpha1.ConditionAllInjected, + Status: corev1.ConditionTrue, + } + selected := v1alpha1.ChaosCondition{ + Type: v1alpha1.ConditionSelected, + Status: corev1.ConditionTrue, + } + paused := v1alpha1.ChaosCondition{ + Type: v1alpha1.ConditionPaused, + Status: corev1.ConditionTrue, + } + + if isConditionTrue(chaosStatus, selected) && isConditionTrue(chaosStatus, allInjected) { + currentStatus = StatusRunning + } else if isConditionTrue(chaosStatus, allRecovered) { + currentStatus = StatusFinished + } else if !isConditionTrue(chaosStatus, paused) && !isConditionTrue(chaosStatus, selected) { + currentStatus = StatusUnknown + } + + // If the status is unknown, always notify listeners + if currentStatus == StatusUnknown { + c.notifyListeners(string(StatusUnknown), nil) + continue + } + + // If the status has changed, update internal status and notify listeners + if c.Status != currentStatus { + c.Status = currentStatus + + switch c.Status { + case StatusCreated: + c.notifyListeners("created", nil) + case StatusRunning: + c.startTime = time.Now() + c.notifyListeners("started", nil) + case StatusPaused: + c.notifyListeners("paused", nil) + case StatusFinished: + c.endTime = time.Now() + c.notifyListeners("finished", nil) + // Delete the chaos object when it finishes + err := c.Delete(context.Background()) + if err != nil { + c.logger.Error().Err(err).Msg("failed to delete chaos object") + } + } + } + } + } +} + +type NetworkChaosOpts struct { + Name string + Description string + DelayCreate time.Duration + Delay *v1alpha1.DelaySpec + Loss *v1alpha1.LossSpec + NodeCount int + Duration time.Duration + Selector v1alpha1.PodSelectorSpec + K8sClient client.Client +} + +func (o *NetworkChaosOpts) Validate() error { + if o.Delay != nil { + latency, err := time.ParseDuration(o.Delay.Latency) + if err != nil { + return fmt.Errorf("invalid latency: %v", err) + } + if latency > 500*time.Millisecond { + return fmt.Errorf("duration should be less than 500ms") + } + } + if o.Loss != nil { + lossInt, err := strconv.Atoi(o.Loss.Loss) // Convert the string to an integer + if err != nil { + return fmt.Errorf("invalid loss value: %s", err) + } + if lossInt > 100 { + return fmt.Errorf("loss should be less than 100") + } + } + if o.Loss == nil && o.Delay == nil { + return fmt.Errorf("either delay or loss should be specified") + } + return nil + +} + +type PodChaosOpts struct { + Name string + Description string + DelayCreate time.Duration + NodeCount int + Duration time.Duration + Spec v1alpha1.PodChaosSpec + K8sClient client.Client +} + +type StressChaosOpts struct { + Name string + Description string + DelayCreate time.Duration + NodeCount int + Stressors *v1alpha1.Stressors + Duration time.Duration + Selector v1alpha1.PodSelectorSpec + K8sClient client.Client +} + +// NewChaosMeshClient initializes and returns a new Kubernetes client configured for Chaos Mesh +func NewChaosMeshClient() (client.Client, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) + config, err := kubeConfig.ClientConfig() + if err != nil { + return nil, errors.Wrap(err, "failed to load kubeconfig") + } + + // Ensure the Chaos Mesh types are added to the scheme + if err := v1alpha1.AddToScheme(scheme.Scheme); err != nil { + return nil, errors.Wrap(err, "could not add the Chaos Mesh scheme") + } + + // Create a new client for the Chaos Mesh API + chaosClient, err := client.New(config, client.Options{Scheme: scheme.Scheme}) + if err != nil { + return nil, errors.Wrap(err, "failed to create a client for Chaos Mesh") + } + + return chaosClient, nil +} diff --git a/havoc/k8schaos/chaos_entity.go b/havoc/k8schaos/chaos_entity.go new file mode 100644 index 000000000..a2f46110c --- /dev/null +++ b/havoc/k8schaos/chaos_entity.go @@ -0,0 +1,27 @@ +package k8schaos + +import ( + "context" + "time" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ChaosEntity is an interface that defines common behaviors for chaos management entities. +type ChaosEntity interface { + // Create initializes and submits the chaos object to Kubernetes. + Create(ctx context.Context) + // Delete removes the chaos object from Kubernetes. + Delete(ctx context.Context) error + // Registers a listener to receive updates about the chaos object's lifecycle. + AddListener(listener ChaosListener) + + GetObject() client.Object + GetChaosName() string + GetChaosDescription() string + GetChaosDuration() (time.Duration, error) + GetChaosSpec() interface{} + GetStartTime() time.Time + GetEndTime() time.Time + GetExpectedEndTime() (time.Time, error) +} diff --git a/havoc/k8schaos/chaos_helper.go b/havoc/k8schaos/chaos_helper.go new file mode 100644 index 000000000..7fbff6caf --- /dev/null +++ b/havoc/k8schaos/chaos_helper.go @@ -0,0 +1,44 @@ +package k8schaos + +import ( + "errors" + "time" +) + +// WaitForAllChaosRunning waits for all chaos experiments to be running +func WaitForAllChaosRunning(chaosObjects []*Chaos, timeoutDuration time.Duration) error { + timeout := time.NewTimer(timeoutDuration) + defer timeout.Stop() + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + runningStatus := make(map[*Chaos]bool) + for _, chaos := range chaosObjects { + runningStatus[chaos] = false + } + + for { + allRunning := true + + select { + case <-timeout.C: + return errors.New("timeout reached before all chaos experiments became running") + case <-ticker.C: + for chaos, isRunning := range runningStatus { + if !isRunning { // Only check if not already marked as running + if chaos.Status == StatusRunning { + runningStatus[chaos] = true + } else { + allRunning = false + } + } + } + + if allRunning { + return nil // All chaos objects are running, can exit + } + // Otherwise, continue the loop + } + } +} diff --git a/havoc/k8schaos/chaos_listener.go b/havoc/k8schaos/chaos_listener.go new file mode 100644 index 000000000..4f8ac44c1 --- /dev/null +++ b/havoc/k8schaos/chaos_listener.go @@ -0,0 +1,12 @@ +package k8schaos + +type ChaosListener interface { + OnChaosCreated(chaos Chaos) + OnChaosCreationFailed(chaos Chaos, reason error) + OnChaosStarted(chaos Chaos) + OnChaosPaused(chaos Chaos) + OnChaosEnded(chaos Chaos) // When the chaos is finished or deleted + OnChaosStatusUnknown(chaos Chaos) // When the chaos status is unknown + OnScheduleCreated(chaos Schedule) + OnScheduleDeleted(chaos Schedule) // When the chaos is finished or deleted +} diff --git a/havoc/k8schaos/console_logger.go b/havoc/k8schaos/console_logger.go new file mode 100644 index 000000000..9bee48d6b --- /dev/null +++ b/havoc/k8schaos/console_logger.go @@ -0,0 +1,140 @@ +package k8schaos + +import ( + "time" + + "github.com/rs/zerolog" +) + +type ChaosLogger struct { + logger zerolog.Logger +} + +func NewChaosLogger(logger zerolog.Logger) *ChaosLogger { + return &ChaosLogger{logger: logger} +} + +func (l ChaosLogger) OnChaosCreated(chaos Chaos) { + l.commonChaosLog("info", chaos).Msg("Chaos created") +} + +func (l ChaosLogger) OnChaosCreationFailed(chaos Chaos, reason error) { + l.commonChaosLog("error", chaos). + Err(reason). + Msg("Failed to create chaos object") +} + +func (l ChaosLogger) OnChaosStarted(chaos Chaos) { + experiment, _ := chaos.GetExperimentStatus() + + l.commonChaosLog("info", chaos). + Interface("spec", chaos.GetChaosSpec()). + Interface("records", experiment.Records). + Msg("Chaos started") +} + +func (l ChaosLogger) OnChaosPaused(chaos Chaos) { + l.commonChaosLog("info", chaos). + Msg("Chaos paused") +} + +func (l ChaosLogger) OnChaosEnded(chaos Chaos) { + l.commonChaosLog("info", chaos). + Msg("Chaos ended") +} + +func (l ChaosLogger) OnChaosDeleted(chaos Chaos) { + l.commonChaosLog("info", chaos). + Msg("Chaos deleted") +} + +type SimplifiedEvent struct { + LastTimestamp string + Type string + Message string +} + +func (l ChaosLogger) OnChaosStatusUnknown(chaos Chaos) { + status, _ := chaos.GetExperimentStatus() + events, _ := chaos.GetChaosEvents() + + // Create a slice to hold the simplified events + simplifiedEvents := make([]SimplifiedEvent, 0, len(events.Items)) + + // Iterate over the events and extract the required information + for _, event := range events.Items { + simplifiedEvents = append(simplifiedEvents, SimplifiedEvent{ + LastTimestamp: event.LastTimestamp.Time.Format(time.RFC3339), + Type: event.Type, + Message: event.Message, + }) + } + + l.commonChaosLog("error", chaos). + Interface("status", status). + Interface("events", simplifiedEvents). + Msg("Chaos status unknown") +} + +func (l ChaosLogger) OnScheduleCreated(schedule Schedule) { + duration, _ := schedule.GetChaosDuration() + + l.logger.Info(). + Str("logger", "chaos"). + Str("name", schedule.GetObject().GetName()). + Str("namespace", schedule.GetObject().GetNamespace()). + Str("description", schedule.GetChaosDescription()). + Str("duration", duration.String()). + Time("startTime", schedule.GetStartTime()). + Time("endTime", schedule.GetEndTime()). + Interface("spec", schedule.GetChaosSpec()). + Msg("Chaos schedule created") +} + +func (l ChaosLogger) OnScheduleDeleted(schedule Schedule) { + duration, _ := schedule.GetChaosDuration() + + l.logger.Info(). + Str("logger", "chaos"). + Str("name", schedule.GetObject().GetName()). + Str("namespace", schedule.GetObject().GetNamespace()). + Str("description", schedule.GetChaosDescription()). + Str("duration", duration.String()). + Time("startTime", schedule.GetStartTime()). + Time("endTime", schedule.GetEndTime()). + Interface("spec", schedule.GetChaosSpec()). + Msg("Chaos schedule deleted") +} + +func (l ChaosLogger) commonChaosLog(logLevel string, chaos Chaos) *zerolog.Event { + // Create a base event based on the dynamic log level + var event *zerolog.Event + switch logLevel { + case "debug": + event = l.logger.Debug() + case "info": + event = l.logger.Info() + case "warn": + event = l.logger.Warn() + case "error": + event = l.logger.Error() + case "fatal": + event = l.logger.Fatal() + case "panic": + event = l.logger.Panic() + default: + // Default to info level if an unknown level is provided + event = l.logger.Info() + } + + duration, _ := chaos.GetChaosDuration() + + return event. + Str("logger", "chaos"). + Str("name", chaos.GetObject().GetName()). + Str("namespace", chaos.GetObject().GetNamespace()). + Str("description", chaos.GetChaosDescription()). + Str("duration", duration.String()). + Time("startTime", chaos.GetStartTime()). + Time("endTime", chaos.GetEndTime()) +} diff --git a/havoc/k8schaos/logger.go b/havoc/k8schaos/logger.go new file mode 100644 index 000000000..58d9e18d5 --- /dev/null +++ b/havoc/k8schaos/logger.go @@ -0,0 +1,44 @@ +package k8schaos + +import ( + "os" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// Default logger +var Logger zerolog.Logger + +func init() { + // Default logger + Logger = CreateLogger(LoggerConfig{ + LogOutput: os.Getenv("CHAOS_LOG_OUTPUT"), + LogLevel: os.Getenv("CHAOS_LOG_LEVEL"), + LogType: "chaos", + }) +} + +type LoggerConfig struct { + LogOutput string // "json-console" for JSON output, empty or "console" for human-friendly console output + LogLevel string // Log level (e.g., "info", "debug", "error") + LogType string // Custom log type identifier +} + +// Create initializes a zerolog.Logger based on the specified configuration. +func CreateLogger(config LoggerConfig) zerolog.Logger { + // Parse the log level + lvl, err := zerolog.ParseLevel(config.LogLevel) + if err != nil { + panic(err) // Consider more graceful error handling based on your application's requirements + } + + switch config.LogOutput { + case "json-console": + // Configure for JSON console output + return zerolog.New(os.Stderr).Level(lvl).With().Timestamp().Str("type", config.LogType).Logger() + default: + // Configure for console (human-friendly) output + return log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"}).Level(lvl).With().Timestamp().Str("type", config.LogType).Logger() + } +} diff --git a/havoc/k8schaos/range_grafana_annotator.go b/havoc/k8schaos/range_grafana_annotator.go new file mode 100644 index 000000000..4d1658197 --- /dev/null +++ b/havoc/k8schaos/range_grafana_annotator.go @@ -0,0 +1,240 @@ +package k8schaos + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/rs/zerolog" + "github.com/smartcontractkit/chainlink-testing-framework/grafana" +) + +type RangeGrafanaAnnotator struct { + client *grafana.Client + dashboardUID string + chaosMap map[string]int64 // Maps Chaos ID to Grafana Annotation ID + logger zerolog.Logger +} + +func NewRangeGrafanaAnnotator(grafanaURL, grafanaToken, dashboardUID string, logger zerolog.Logger) *RangeGrafanaAnnotator { + return &RangeGrafanaAnnotator{ + client: grafana.NewGrafanaClient(grafanaURL, grafanaToken), + dashboardUID: dashboardUID, + chaosMap: make(map[string]int64), + logger: logger, + } +} + +func (l RangeGrafanaAnnotator) OnChaosCreated(chaos Chaos) { +} + +func (l RangeGrafanaAnnotator) OnChaosStarted(chaos Chaos) { + experiment, _ := chaos.GetExperimentStatus() + duration, _ := chaos.GetChaosDuration() + + var sb strings.Builder + sb.WriteString("") + sb.WriteString(fmt.Sprintf("

%s Started

", chaos.GetChaosTypeStr())) + sb.WriteString(fmt.Sprintf("
Name: %s
", chaos.Object.GetName())) + if chaos.Description != "" { + sb.WriteString(fmt.Sprintf("
Description: %s
", chaos.Description)) + } + sb.WriteString(fmt.Sprintf("
Start Time: %s
", chaos.GetStartTime().Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
Duration: %s
", duration.String())) + + spec := chaos.GetChaosSpec() + specBytes, err := json.MarshalIndent(spec, "", " ") + if err == nil && len(specBytes) > 0 { + sb.WriteString("
") + sb.WriteString("
Spec:
") + sb.WriteString(string(specBytes)) + sb.WriteString("
") + } else { + l.logger.Warn().Msgf("could not get chaos spec: %s", err) + } + + if len(experiment.Records) > 0 { + sb.WriteString("
") + sb.WriteString("
Records:
") + sb.WriteString("
    ") + for _, record := range experiment.Records { + sb.WriteString(fmt.Sprintf("
  • %s: %s
  • ", record.Id, record.Phase)) + } + sb.WriteString("
") + } + + sb.WriteString("") + + a := grafana.PostAnnotation{ + DashboardUID: l.dashboardUID, + Time: Ptr[time.Time](chaos.GetStartTime()), + Text: sb.String(), + } + res, _, err := l.client.PostAnnotation(a) + if err != nil { + l.logger.Warn().Msgf("could not annotate on Grafana: %s", err) + } + + l.chaosMap[chaos.GetChaosName()] = res.ID +} + +func (l RangeGrafanaAnnotator) OnChaosPaused(chaos Chaos) { +} + +func (l RangeGrafanaAnnotator) OnChaosEnded(chaos Chaos) { + annotationID, exists := l.chaosMap[chaos.GetChaosName()] + if !exists { + l.logger.Error().Msgf("No Grafana annotation ID found for Chaos: %s", chaos.GetChaosName()) + return + } + + experiment, _ := chaos.GetExperimentStatus() + duration, _ := chaos.GetChaosDuration() + + var sb strings.Builder + sb.WriteString("") + sb.WriteString(fmt.Sprintf("

%s

", chaos.GetChaosTypeStr())) + sb.WriteString(fmt.Sprintf("
Name: %s
", chaos.Object.GetName())) + if chaos.Description != "" { + sb.WriteString(fmt.Sprintf("
Description: %s
", chaos.Description)) + } + sb.WriteString(fmt.Sprintf("
Start Time: %s
", chaos.GetStartTime().Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
End Time: %s
", chaos.GetEndTime().Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
Duration: %s
", duration.String())) + + spec := chaos.GetChaosSpec() + specBytes, err := json.MarshalIndent(spec, "", " ") + if err == nil && len(specBytes) > 0 { + sb.WriteString("
") + sb.WriteString("
Spec:
") + sb.WriteString(string(specBytes)) + sb.WriteString("
") + } else { + l.logger.Warn().Msgf("could not get chaos spec: %s", err) + } + + if len(experiment.Records) > 0 { + sb.WriteString("
") + sb.WriteString("
Records:
") + sb.WriteString("
    ") + for _, record := range experiment.Records { + sb.WriteString(fmt.Sprintf("
  • %s: %s
  • ", record.Id, record.Phase)) + } + sb.WriteString("
") + } + + sb.WriteString("") + + // Delete the temporary start annotation + _, err = l.client.DeleteAnnotation(annotationID) + if err != nil { + l.logger.Error().Msgf("could not delete temporary start annotation: %s", err) + } + delete(l.chaosMap, chaos.GetChaosName()) + + // Create the final annotation (time range) + a := grafana.PostAnnotation{ + DashboardUID: l.dashboardUID, + Time: Ptr[time.Time](chaos.GetStartTime()), + TimeEnd: Ptr[time.Time](chaos.GetEndTime()), + Text: sb.String(), + } + res, _, err := l.client.PostAnnotation(a) + if err != nil { + l.logger.Warn().Msgf("could not annotate on Grafana: %s", err) + } + l.chaosMap[chaos.GetChaosName()] = res.ID +} + +func (l RangeGrafanaAnnotator) OnChaosStatusUnknown(chaos Chaos) { +} + +func (l RangeGrafanaAnnotator) OnScheduleCreated(chaos Schedule) { + var sb strings.Builder + sb.WriteString("") + sb.WriteString(fmt.Sprintf("

%s Schedule Created

", chaos.Object.Spec.Type)) + sb.WriteString(fmt.Sprintf("
Name: %s
", chaos.Object.ObjectMeta.Name)) + sb.WriteString(fmt.Sprintf("
Schedule: %s
", chaos.Object.Spec.Schedule)) + if chaos.Description != "" { + sb.WriteString(fmt.Sprintf("
Description: %s
", chaos.Description)) + } + sb.WriteString(fmt.Sprintf("
Start Time: %s
", chaos.startTime.Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
Duration: %s
", chaos.Duration.String())) + + spec := chaos.Object.Spec.ScheduleItem + specBytes, err := json.MarshalIndent(spec, "", " ") + if err == nil && len(specBytes) > 0 { + sb.WriteString("
") + sb.WriteString("
Spec:
") + sb.WriteString(string(specBytes)) + sb.WriteString("
") + } else { + l.logger.Warn().Msgf("could not get chaos spec: %s", err) + } + sb.WriteString("") + + a := grafana.PostAnnotation{ + DashboardUID: l.dashboardUID, + Time: Ptr[time.Time](chaos.startTime), + Text: sb.String(), + } + res, _, err := l.client.PostAnnotation(a) + if err != nil { + l.logger.Warn().Msgf("could not annotate on Grafana: %s", err) + } + + l.chaosMap[chaos.Object.GetName()] = res.ID +} + +func (l RangeGrafanaAnnotator) OnScheduleDeleted(chaos Schedule) { + annotationID, exists := l.chaosMap[chaos.Object.GetName()] + if !exists { + l.logger.Error().Msgf("No Grafana annotation ID found for Chaos: %s", chaos.Object.GetName()) + return + } + + var sb strings.Builder + sb.WriteString("") + sb.WriteString(fmt.Sprintf("

%s Schedule

", chaos.Object.Spec.Type)) + sb.WriteString(fmt.Sprintf("
Name: %s
", chaos.Object.ObjectMeta.Name)) + sb.WriteString(fmt.Sprintf("
Schedule: %s
", chaos.Object.Spec.Schedule)) + if chaos.Description != "" { + sb.WriteString(fmt.Sprintf("
Description: %s
", chaos.Description)) + } + sb.WriteString(fmt.Sprintf("
Start Time: %s
", chaos.startTime.Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
End Time: %s
", chaos.endTime.Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
Duration: %s
", chaos.Duration.String())) + + spec := chaos.Object.Spec.ScheduleItem + specBytes, err := json.MarshalIndent(spec, "", " ") + if err == nil && len(specBytes) > 0 { + sb.WriteString("
") + sb.WriteString("
Spec:
") + sb.WriteString(string(specBytes)) + sb.WriteString("
") + } else { + l.logger.Warn().Msgf("could not get chaos spec: %s", err) + } + sb.WriteString("") + + // Delete the temporary start annotation + _, err = l.client.DeleteAnnotation(annotationID) + if err != nil { + l.logger.Error().Msgf("could not delete temporary start annotation: %s", err) + } + delete(l.chaosMap, chaos.Object.GetName()) + + // Create the final annotation (time range) + a := grafana.PostAnnotation{ + DashboardUID: l.dashboardUID, + Time: Ptr[time.Time](chaos.startTime), + TimeEnd: Ptr[time.Time](chaos.endTime), + Text: sb.String(), + } + res, _, err := l.client.PostAnnotation(a) + if err != nil { + l.logger.Warn().Msgf("could not annotate on Grafana: %s", err) + } + l.chaosMap[chaos.Object.GetName()] = res.ID +} diff --git a/havoc/k8schaos/schedule.go b/havoc/k8schaos/schedule.go new file mode 100644 index 000000000..f8cd543da --- /dev/null +++ b/havoc/k8schaos/schedule.go @@ -0,0 +1,225 @@ +package k8schaos + +import ( + "context" + "time" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ScheduleStatus string + +const ( + ScheduleStatusCreated ScheduleStatus = "created" + ScheduleStatusDeleted ScheduleStatus = "deleted" + ScheduleStatusUnknown ScheduleStatus = "unknown" // For any state that doesn't match the above +) + +type Schedule struct { + Object *v1alpha1.Schedule + Description string + DelayCreate time.Duration // Delay before creating the chaos object + Duration time.Duration // Duration for which the chaos object should exist + Status ChaosStatus + Client client.Client + listeners []ChaosListener + cancelMonitor context.CancelFunc + startTime time.Time + endTime time.Time + logger *zerolog.Logger +} + +type ScheduleOpts struct { + Object *v1alpha1.Schedule + Description string + DelayCreate time.Duration + Duration time.Duration + Client client.Client + Listeners []ChaosListener + Logger *zerolog.Logger +} + +func NewSchedule(opts ScheduleOpts) (*Schedule, error) { + if opts.Client == nil { + return nil, errors.New("client is required") + } + if opts.Object == nil { + return nil, errors.New("chaos object is required") + } + if opts.Logger == nil { + return nil, errors.New("logger is required") + } + + return &Schedule{ + Object: opts.Object, + Description: opts.Description, + DelayCreate: opts.DelayCreate, + Duration: opts.Duration, + Client: opts.Client, + listeners: opts.Listeners, + logger: opts.Logger, + }, nil +} + +// Create initiates a delayed creation of a chaos object, respecting context cancellation and deletion requests. +// It uses a timer based on `DelayCreate` and calls `create` method upon expiration unless preempted by deletion. +func (s *Schedule) Create(ctx context.Context) { + done := make(chan struct{}) + + // Create the timer with the delay to create the chaos object + timer := time.NewTimer(s.DelayCreate) + + go func() { + select { + case <-ctx.Done(): + // If the context is canceled, stop the timer and exit + if !timer.Stop() { + <-timer.C // If the timer already expired, drain the channel + } + close(done) // Signal that the operation was canceled + case <-timer.C: + // Timer expired, check if deletion was not requested + if s.Status != StatusDeleted { + s.createNow(ctx) + } + close(done) // Signal that the creation process is either done or skipped + } + }() +} + +func (s *Schedule) Delete(ctx context.Context) error { + if err := s.Client.Delete(ctx, s.Object); err != nil { + return errors.Wrap(err, "failed to delete chaos object") + } + + // Cancel the monitoring goroutine + if s.cancelMonitor != nil { + s.cancelMonitor() + } + + s.endTime = time.Now() + s.Status = StatusDeleted + s.notifyListeners(string(ScheduleStatusDeleted)) + + return nil +} + +func (s *Schedule) AddListener(listener ChaosListener) { + s.listeners = append(s.listeners, listener) +} + +func (s *Schedule) GetObject() client.Object { + return s.Object +} + +func (s *Schedule) GetChaosName() string { + return s.Object.GetName() +} + +func (s *Schedule) GetChaosDescription() string { + return s.Description +} + +func (s *Schedule) GetChaosSpec() interface{} { + return s.Object.Spec.ScheduleItem +} + +func (s *Schedule) GetChaosDuration() (time.Duration, error) { + return s.Duration, nil +} + +func (s *Schedule) GetStartTime() time.Time { + return s.startTime +} + +func (s *Schedule) GetEndTime() time.Time { + return s.endTime +} + +func (s *Schedule) GetExpectedEndTime() (time.Time, error) { + duration, err := s.GetChaosDuration() + if err != nil { + return time.Time{}, err + } + return s.startTime.Add(duration), nil +} + +func (s *Schedule) createNow(ctx context.Context) { + if err := s.Client.Create(ctx, s.Object); err != nil { + Logger.Error().Err(err).Interface("chaos", s).Msg("failed to create chaos object") + return + } + s.startTime = time.Now() + s.Status = StatusCreated + s.notifyListeners(string(ScheduleStatusCreated)) + + // Create a cancellable context for monitorStatus + monitorCtx, cancel := context.WithCancel(ctx) + s.cancelMonitor = cancel + go s.monitorStatus(monitorCtx) + + // Start a deletion timer to delete the chaos object after the specified duration + done := make(chan struct{}) + deleteTimer := time.NewTimer(s.Duration) + go func() { + select { + case <-ctx.Done(): + // Context was canceled, ensure chaos object is deleted + if !deleteTimer.Stop() { + <-deleteTimer.C // Drain the timer if it already fired + } + err := s.Delete(context.Background()) + if err != nil { + s.logger.Error().Err(err).Msg("failed to delete chaos object") + } + close(done) + case <-deleteTimer.C: + // Duration elapsed, delete the chaos object + err := s.Delete(context.Background()) + if err != nil { + s.logger.Error().Err(err).Msg("failed to delete chaos object") + } + close(done) + } + }() +} + +func (s *Schedule) notifyListeners(event string) { + for _, listener := range s.listeners { + switch event { + case string(ScheduleStatusCreated): + listener.OnScheduleCreated(*s) + case string(ScheduleStatusDeleted): + listener.OnScheduleDeleted(*s) + } + } +} + +func (s *Schedule) monitorStatus(ctx context.Context) { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + // Context canceled, stop monitoring + return + case <-ticker.C: + // Fetch the latest state of the Schedule object + var schedule v1alpha1.Schedule + if err := s.Client.Get(ctx, client.ObjectKey{ + Namespace: s.Object.GetNamespace(), + Name: s.Object.GetName(), + }, &schedule); err != nil { + Logger.Error().Err(err).Msg("Failed to get Schedule object") + continue + } + + // Log or process the schedule's current status + // Loggerog.Info().Interface("status", schedule.Status).Msg("Current Schedule Status") + } + } +} diff --git a/havoc/k8schaos/single_line_grafana_annotator.go b/havoc/k8schaos/single_line_grafana_annotator.go new file mode 100644 index 000000000..9afc91102 --- /dev/null +++ b/havoc/k8schaos/single_line_grafana_annotator.go @@ -0,0 +1,205 @@ +package k8schaos + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/rs/zerolog" + "github.com/smartcontractkit/chainlink-testing-framework/grafana" +) + +type SingleLineGrafanaAnnotator struct { + client *grafana.Client + dashboardUID string + logger zerolog.Logger +} + +func NewSingleLineGrafanaAnnotator(grafanaURL, grafanaToken, dashboardUID string, logger zerolog.Logger) *SingleLineGrafanaAnnotator { + return &SingleLineGrafanaAnnotator{ + client: grafana.NewGrafanaClient(grafanaURL, grafanaToken), + dashboardUID: dashboardUID, + logger: logger, + } +} + +func (l SingleLineGrafanaAnnotator) OnChaosCreated(chaos Chaos) { +} + +func (l SingleLineGrafanaAnnotator) OnChaosCreationFailed(chaos Chaos, reason error) { +} + +func (l SingleLineGrafanaAnnotator) OnChaosStarted(chaos Chaos) { + experiment, _ := chaos.GetExperimentStatus() + duration, _ := chaos.GetChaosDuration() + + var sb strings.Builder + sb.WriteString("") + sb.WriteString(fmt.Sprintf("

%s Started

", chaos.GetChaosTypeStr())) + sb.WriteString(fmt.Sprintf("
Name: %s
", chaos.Object.GetName())) + if chaos.Description != "" { + sb.WriteString(fmt.Sprintf("
Description: %s
", chaos.Description)) + } + sb.WriteString(fmt.Sprintf("
Start Time: %s
", chaos.GetStartTime().Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
Duration: %s
", duration.String())) + + spec := chaos.GetChaosSpec() + specBytes, err := json.MarshalIndent(spec, "", " ") + if err == nil && len(specBytes) > 0 { + sb.WriteString("
") + sb.WriteString("
Spec:
") + sb.WriteString(string(specBytes)) + sb.WriteString("
") + } else { + l.logger.Warn().Msgf("could not get chaos spec: %s", err) + } + + if len(experiment.Records) > 0 { + sb.WriteString("
") + sb.WriteString("
Records:
") + sb.WriteString("
    ") + for _, record := range experiment.Records { + sb.WriteString(fmt.Sprintf("
  • %s: %s
  • ", record.Id, record.Phase)) + } + sb.WriteString("
") + } + + sb.WriteString("") + + a := grafana.PostAnnotation{ + DashboardUID: l.dashboardUID, + Time: Ptr[time.Time](chaos.GetStartTime()), + Text: sb.String(), + } + _, _, err = l.client.PostAnnotation(a) + if err != nil { + l.logger.Warn().Msgf("could not annotate on Grafana: %s", err) + } +} + +func (l SingleLineGrafanaAnnotator) OnChaosPaused(chaos Chaos) { +} + +func (l SingleLineGrafanaAnnotator) OnChaosEnded(chaos Chaos) { + experiment, _ := chaos.GetExperimentStatus() + duration, _ := chaos.GetChaosDuration() + + var sb strings.Builder + sb.WriteString("") + sb.WriteString(fmt.Sprintf("

%s Ended

", chaos.GetChaosTypeStr())) + sb.WriteString(fmt.Sprintf("
Name: %s
", chaos.Object.GetName())) + if chaos.Description != "" { + sb.WriteString(fmt.Sprintf("
Description: %s
", chaos.Description)) + } + sb.WriteString(fmt.Sprintf("
Start Time: %s
", chaos.GetStartTime().Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
End Time: %s
", chaos.GetEndTime().Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
Duration: %s
", duration.String())) + + spec := chaos.GetChaosSpec() + specBytes, err := json.MarshalIndent(spec, "", " ") + if err == nil && len(specBytes) > 0 { + sb.WriteString("
") + sb.WriteString("
Spec:
") + sb.WriteString(string(specBytes)) + sb.WriteString("
") + } else { + l.logger.Warn().Msgf("could not get chaos spec: %s", err) + } + + if len(experiment.Records) > 0 { + sb.WriteString("
") + sb.WriteString("
Records:
") + sb.WriteString("
    ") + for _, record := range experiment.Records { + sb.WriteString(fmt.Sprintf("
  • %s: %s
  • ", record.Id, record.Phase)) + } + sb.WriteString("
") + } + + sb.WriteString("") + + a := grafana.PostAnnotation{ + DashboardUID: l.dashboardUID, + Time: Ptr[time.Time](chaos.GetEndTime()), + Text: sb.String(), + } + _, _, err = l.client.PostAnnotation(a) + if err != nil { + l.logger.Warn().Msgf("could not annotate on Grafana: %s", err) + } +} + +func (l SingleLineGrafanaAnnotator) OnChaosStatusUnknown(chaos Chaos) { +} + +func (l SingleLineGrafanaAnnotator) OnScheduleCreated(s Schedule) { + var sb strings.Builder + sb.WriteString("") + sb.WriteString(fmt.Sprintf("

%s Schedule Created

", s.Object.Spec.Type)) + sb.WriteString(fmt.Sprintf("
Name: %s
", s.Object.ObjectMeta.Name)) + sb.WriteString(fmt.Sprintf("
Schedule: %s
", s.Object.Spec.Schedule)) + if s.Description != "" { + sb.WriteString(fmt.Sprintf("
Description: %s
", s.Description)) + } + sb.WriteString(fmt.Sprintf("
Start Time: %s
", s.startTime.Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
Duration: %s
", s.Duration.String())) + + spec := s.Object.Spec.ScheduleItem + specBytes, err := json.MarshalIndent(spec, "", " ") + if err == nil && len(specBytes) > 0 { + sb.WriteString("
") + sb.WriteString("
Schedule Spec:
") + sb.WriteString(string(specBytes)) + sb.WriteString("
") + } else { + l.logger.Warn().Msgf("could not get chaos spec: %s", err) + } + sb.WriteString("") + + a := grafana.PostAnnotation{ + DashboardUID: l.dashboardUID, + Time: Ptr[time.Time](s.startTime), + Text: sb.String(), + } + _, _, err = l.client.PostAnnotation(a) + if err != nil { + l.logger.Warn().Msgf("could not annotate on Grafana: %s", err) + } +} + +func (l SingleLineGrafanaAnnotator) OnScheduleDeleted(s Schedule) { + var sb strings.Builder + sb.WriteString("") + sb.WriteString(fmt.Sprintf("

%s Schedule Ended

", s.Object.Spec.Type)) + sb.WriteString(fmt.Sprintf("
Name: %s
", s.Object.ObjectMeta.Name)) + sb.WriteString(fmt.Sprintf("
Schedule: %s
", s.Object.Spec.Schedule)) + if s.Description != "" { + sb.WriteString(fmt.Sprintf("
Description: %s
", s.Description)) + } + sb.WriteString(fmt.Sprintf("
Start Time: %s
", s.startTime.Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
End Time: %s
", s.endTime.Format(time.RFC3339))) + sb.WriteString(fmt.Sprintf("
Duration: %s
", s.Duration.String())) + + spec := s.Object.Spec.ScheduleItem + specBytes, err := json.MarshalIndent(spec, "", " ") + if err == nil && len(specBytes) > 0 { + sb.WriteString("
") + sb.WriteString("
Schedule Spec:
") + sb.WriteString(string(specBytes)) + sb.WriteString("
") + } else { + l.logger.Warn().Msgf("could not get chaos spec: %s", err) + } + sb.WriteString("") + + a := grafana.PostAnnotation{ + DashboardUID: l.dashboardUID, + Time: Ptr[time.Time](s.endTime), + Text: sb.String(), + } + _, _, err = l.client.PostAnnotation(a) + if err != nil { + l.logger.Warn().Msgf("could not annotate on Grafana: %s", err) + } +} diff --git a/havoc/k8schaos/utils.go b/havoc/k8schaos/utils.go new file mode 100644 index 000000000..c0d3b8ac4 --- /dev/null +++ b/havoc/k8schaos/utils.go @@ -0,0 +1,5 @@ +package k8schaos + +func Ptr[T any](value T) *T { + return &value +} diff --git a/havoc/monkey.go b/havoc/monkey.go new file mode 100644 index 000000000..b80229bc9 --- /dev/null +++ b/havoc/monkey.go @@ -0,0 +1,222 @@ +package havoc + +import ( + "context" + "fmt" + "math/rand" + "sync" + "time" + + "github.com/go-resty/resty/v2" + "github.com/pkg/errors" +) + +const ( + MonkeyModeSeq = "seq" + MonkeyModeRandom = "rand" + + ErrInvalidMode = "monkey mode is invalid, should be either \"seq\" or \"rand\"" +) + +type ExperimentAction struct { + Name string + ExperimentKind string + ExperimentSpec string + TimeStart int64 + TimeEnd int64 +} + +type ExperimentAnnotationBody struct { + DashboardUID string `json:"dashboardUID"` + Time int64 `json:"time"` + TimeEnd int64 `json:"timeEnd"` + Tags []string `json:"tags"` + Text string `json:"text"` +} + +type Controller struct { + cfg *Config + client *resty.Client + ctx context.Context + cancel context.CancelFunc + wg *sync.WaitGroup + errors []error + experimentActions []*ExperimentAction +} + +func NewController(cfg *Config) (*Controller, error) { + InitDefaultLogging() + if cfg == nil { + cfg = DefaultConfig() + dumpConfig(cfg) + } + c := resty.New() + c.SetBaseURL(cfg.Havoc.Grafana.URL) + c.SetAuthScheme("Bearer") + c.SetAuthToken(cfg.Havoc.Grafana.Token) + return &Controller{ + client: c, + cfg: cfg, + wg: &sync.WaitGroup{}, + errors: make([]error, 0), + experimentActions: make([]*ExperimentAction, 0), + }, nil +} + +// AnnotateExperiment sends annotation marker to Grafana dashboard +func (m *Controller) AnnotateExperiment(a *ExperimentAction) error { + if m.cfg.Havoc.Grafana.URL == "" || m.cfg.Havoc.Grafana.Token == "" { + L.Warn().Msg("Dashboards are not selected, experiment time wasn't annotated, please check README to enable Grafana integration") + return nil + } + for _, dashboardUID := range m.cfg.Havoc.Grafana.DashboardUIDs { + start := a.TimeStart * 1e3 + end := a.TimeEnd * 1e3 + specBody := fmt.Sprintf("
%s
", a.ExperimentSpec) + aa := &ExperimentAnnotationBody{ + DashboardUID: dashboardUID, + Time: start, + TimeEnd: end, + Tags: []string{"havoc", a.ExperimentKind}, + Text: fmt.Sprintf( + "File: %s\n%s", + a.Name, + specBody, + ), + } + _, err := m.client.R(). + SetBody(aa). + Post(fmt.Sprintf("%s/api/annotations", m.cfg.Havoc.Grafana.URL)) + if err != nil { + return err + } + L.Info(). + Str("DashboardUID", dashboardUID). + Str("Name", a.Name). + Int64("Start", a.TimeStart). + Int64("End", a.TimeEnd). + Msg("Annotated experiment") + } + return nil +} + +func (m *Controller) ApplyAndAnnotate(exp *NamedExperiment) error { + ea := &ExperimentAction{ + Name: exp.Name, + ExperimentKind: exp.Kind, + ExperimentSpec: string(exp.CRDBytes), + TimeStart: time.Now().Unix(), + } + if err := m.ApplyExperiment(exp, true); err != nil { + return err + } + ea.TimeEnd = time.Now().Unix() + return m.AnnotateExperiment(ea) +} + +func (m *Controller) Run() error { + L.Info().Msg("Starting chaos monkey") + dur, err := time.ParseDuration(m.cfg.Havoc.Monkey.Duration) + if err != nil { + return err + } + m.ctx, m.cancel = context.WithTimeout(context.Background(), dur) + defer m.cancel() + existingExperimentTypes, err := m.readExistingExperimentTypes(m.cfg.Havoc.Dir) + if err != nil { + m.errors = append(m.errors, err) + return err + } + + m.wg.Add(1) + switch m.cfg.Havoc.Monkey.Mode { + case MonkeyModeSeq: + for _, expType := range existingExperimentTypes { + experiments, err := m.ReadExperimentsFromDir([]string{expType}, m.cfg.Havoc.Dir) + if err != nil { + m.errors = append(m.errors, err) + return err + } + for _, exp := range experiments { + if err := m.ApplyAndAnnotate(exp); err != nil { + m.errors = append(m.errors, err) + return err + } + cdDuration, err := time.ParseDuration(m.cfg.Havoc.Monkey.Cooldown) + if err != nil { + m.errors = append(m.errors, err) + return err + } + select { + case <-m.ctx.Done(): + m.wg.Done() + L.Info().Msg("Monkey has finished by timeout") + return nil + default: + } + L.Info(). + Dur("Duration", cdDuration). + Msg("Cooldown between experiments") + time.Sleep(cdDuration) + } + } + L.Info().Msg("Monkey has finished all scheduled experiments") + m.wg.Done() + case MonkeyModeRandom: + allExperiments := make([]*NamedExperiment, 0) + r := rand.New(rand.NewSource(time.Now().Unix())) + for _, expType := range existingExperimentTypes { + experiments, err := m.ReadExperimentsFromDir([]string{expType}, m.cfg.Havoc.Dir) + if err != nil { + m.errors = append(m.errors, err) + return err + } + allExperiments = append(allExperiments, experiments...) + } + for { + select { + case <-m.ctx.Done(): + m.wg.Done() + L.Info().Msg("Monkey has finished by timeout") + return nil + default: + exp := pickExperiment(r, allExperiments) + if err := m.ApplyAndAnnotate(exp); err != nil { + m.errors = append(m.errors, err) + return err + } + cdDuration, err := time.ParseDuration(m.cfg.Havoc.Monkey.Cooldown) + if err != nil { + m.errors = append(m.errors, err) + return err + } + L.Info(). + Dur("Duration", cdDuration). + Msg("Cooldown between experiments") + time.Sleep(cdDuration) + } + } + default: + return errors.New(ErrInvalidMode) + } + return nil +} + +func (m *Controller) Stop() []error { + L.Info().Msg("Stopping chaos monkey") + m.cancel() + m.wg.Wait() + L.Info().Errs("Errors", m.errors).Msg("Chaos monkey stopped") + return m.errors +} + +func (m *Controller) Wait() []error { + L.Info().Msg("Waiting for chaos monkey to finish") + m.wg.Wait() + L.Info().Errs("Errors", m.errors).Msg("Chaos monkey finished") + return m.errors +} + +func pickExperiment(r *rand.Rand, s []*NamedExperiment) *NamedExperiment { + return s[r.Intn(len(s))] +} diff --git a/havoc/openapi.go b/havoc/openapi.go new file mode 100644 index 000000000..d5450bc22 --- /dev/null +++ b/havoc/openapi.go @@ -0,0 +1,140 @@ +package havoc + +import ( + "fmt" + "github.com/getkin/kin-openapi/openapi3" + "github.com/pkg/errors" + "github.com/samber/lo" + "regexp" + "strings" +) + +const ( + ErrParsingOpenAPISpec = "failed to parse OpenAPISpec" +) + +var ( + OpenAPIPathParam = regexp.MustCompile(`({.*})`) +) + +type OAPISpecData struct { + Port int64 + RawPaths []string + SpecData map[string]*openapi3.PathItem +} + +// ParseOpenAPISpecs parses OpenAPI spec methods +func (m *Controller) ParseOpenAPISpecs() ([]*OAPISpecData, error) { + data := make([]*OAPISpecData, 0) + for _, oapiData := range m.cfg.Havoc.OpenAPI.Mapping { + for _, p := range oapiData.SpecToPortMappings { + loader := openapi3.NewLoader() + doc, err := loader.LoadFromFile(p.Path) + if err != nil { + return nil, errors.Wrap(err, ErrParsingOpenAPISpec) + } + oa := &OAPISpecData{ + Port: p.Port, + RawPaths: make([]string, 0), + SpecData: doc.Paths.Map(), + } + for rawPath := range doc.Paths.Map() { + L.Info().Str("Path", rawPath).Msg("Found API path") + oa.RawPaths = append(oa.RawPaths, rawPath) + } + data = append(data, oa) + } + } + return data, nil +} + +// generateOAPIExperiments generates HTTP experiments for a component group (entry), for each method type +func (m *Controller) generateOAPIExperiments(experiments map[string]string, namespace string, entry lo.Entry[string, int], oapiSpecs []*OAPISpecData) error { + for _, apiSpec := range oapiSpecs { + for _, rawPath := range apiSpec.RawPaths { + pathData := apiSpec.SpecData[rawPath] + if pathData.Connect != nil { + if err := m.generateHTTPExperiment(experiments, namespace, entry, rawPath, "CONNECT", apiSpec.Port); err != nil { + return err + } + } + if pathData.Delete != nil { + if err := m.generateHTTPExperiment(experiments, namespace, entry, rawPath, "DELETE", apiSpec.Port); err != nil { + return err + } + } + if pathData.Get != nil { + if err := m.generateHTTPExperiment(experiments, namespace, entry, rawPath, "GET", apiSpec.Port); err != nil { + return err + } + } + if pathData.Head != nil { + if err := m.generateHTTPExperiment(experiments, namespace, entry, rawPath, "HEAD", apiSpec.Port); err != nil { + return err + } + } + if pathData.Options != nil { + if err := m.generateHTTPExperiment(experiments, namespace, entry, rawPath, "OPTIONS", apiSpec.Port); err != nil { + return err + } + } + if pathData.Patch != nil { + if err := m.generateHTTPExperiment(experiments, namespace, entry, rawPath, "PATCH", apiSpec.Port); err != nil { + return err + } + } + if pathData.Post != nil { + if err := m.generateHTTPExperiment(experiments, namespace, entry, rawPath, "POST", apiSpec.Port); err != nil { + return err + } + } + if pathData.Put != nil { + if err := m.generateHTTPExperiment(experiments, namespace, entry, rawPath, "PUT", apiSpec.Port); err != nil { + return err + } + } + if pathData.Trace != nil { + if err := m.generateHTTPExperiment(experiments, namespace, entry, rawPath, "TRACE", apiSpec.Port); err != nil { + return err + } + } + } + } + return nil +} + +func (m *Controller) generateHTTPExperiment( + experiments map[string]string, + namespace string, + entry lo.Entry[string, int], + rawPath string, + method string, + port int64, +) error { + sanitizedLabel := sanitizeLabel(entry.Key) + sanitizedRawPath := sanitizeLabel(rawPath) + sanitizedLabel = fmt.Sprintf("%s-%s-%s", sanitizedLabel, sanitizedRawPath, method) + experiment, err := HTTPExperiment{ + Namespace: namespace, + ExperimentName: strings.ToLower(fmt.Sprintf("%s-%s", ChaosTypeHTTP, sanitizedLabel)), + Duration: m.cfg.Havoc.StressCPU.Duration, + Mode: "all", + Selector: entry.Key, + Target: "Response", + Abort: true, + Path: pathToWildcardExpr(rawPath), + Method: method, + Port: port, + }.String() + if err != nil { + return err + } + experiments[sanitizedLabel] = experiment + return nil +} + +// pathToWildcardExpr transforms path params into wildcard expressions +// TODO: this need thorough testing though, since it can be much more complex +func pathToWildcardExpr(path string) string { + return string(OpenAPIPathParam.ReplaceAll([]byte(path), []byte("*"))) +} diff --git a/havoc/oscmd.go b/havoc/oscmd.go new file mode 100644 index 000000000..a168cadbb --- /dev/null +++ b/havoc/oscmd.go @@ -0,0 +1,39 @@ +package havoc + +import ( + "bytes" + "context" + "errors" + "os/exec" + "strings" +) + +func ExecCmd(command string) (string, error) { + L.Info().Interface("Command", command).Msg("Executing command") + c := strings.Split(command, " ") + cmd := exec.CommandContext(context.Background(), c[0], c[1:]...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + exitCode := exitErr.ExitCode() + L.Error(). + Int("Code", exitCode). + Msg("Command exited with status code") + L.Error(). + Str("Out", stdout.String()). + Str("Err", stderr.String()). + Msg("Command output") + } + } else { + L.Info().Msg("Command ran successfully") + L.Debug(). + Str("Out", stdout.String()). + Str("Err", stderr.String()). + Msg("Command output") + } + return stdout.String(), err +} diff --git a/havoc/parse.go b/havoc/parse.go new file mode 100644 index 000000000..7cf146a9e --- /dev/null +++ b/havoc/parse.go @@ -0,0 +1,181 @@ +package havoc + +import ( + "encoding/json" + "fmt" + "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/samber/lo" + "os" + "sort" + "strings" +) + +const ( + ErrNoNamespace = "no namespace found" + ErrEmptyNamespace = "no pods found inside namespace, namespace is empty or check your filter" +) + +const ( + NoGroupKey = "no-group" +) + +type ManifestPart struct { + Kind string + Name string + FlattenedManifest map[string]interface{} +} + +// PodsListResponse pod list response from kubectl in JSON +type PodsListResponse struct { + Items []*PodResponse `json:"items"` +} + +// PodResponse pod info response from kubectl in JSON +type PodResponse struct { + Metadata struct { + Name string `json:"name"` + Labels map[string]string `json:"labels"` + } `json:"metadata"` +} + +type GroupInfo struct { + Label string + PodsAffected int +} + +// ActionablePodInfo info about pod and labels for which we can generate a chaos experiment +type ActionablePodInfo struct { + PodName string + Labels []string + HasGroup bool +} + +func uniquePairs(strings []string) [][]string { + var pairs [][]string + for i := 0; i < len(strings); i++ { + for j := i + 1; j < len(strings); j++ { + pair := []string{strings[i], strings[j]} + pairs = append(pairs, pair) + } + } + return pairs +} + +func (m *Controller) processPodInfoLo(plr *PodsListResponse) (map[string][]*PodResponse, []*PodResponse, []lo.Entry[string, int], [][]string, error) { + L.Info().Msg("Processing pods info") + // filtering + filteredPods := lo.Filter(plr.Items, func(item *PodResponse, index int) bool { + return !sliceContainsSubString(item.Metadata.Name, m.cfg.Havoc.IgnoredPods) + }) + labelsToAllow := append([]string{}, m.cfg.Havoc.ComponentLabelKey) + if m.hasNetworkExperiments() { + labelsToAllow = append(labelsToAllow, m.cfg.Havoc.NetworkPartition.Label) + } + for _, p := range filteredPods { + p.Metadata.Labels = lo.PickByKeys(p.Metadata.Labels, labelsToAllow) + } + if len(filteredPods) == 0 { + return nil, nil, nil, nil, errors.New(ErrEmptyNamespace) + } + // grouping + byComponent := lo.GroupBy(filteredPods, func(item *PodResponse) string { + key := m.cfg.Havoc.ComponentLabelKey + return m.labelSelector(key, item.Metadata.Labels[key]) + }) + var byPartition map[string][]*PodResponse + if m.hasNetworkExperiments() { + byPartition = lo.GroupBy(filteredPods, func(item *PodResponse) string { + key := m.cfg.Havoc.NetworkPartition.Label + return m.labelSelector(key, item.Metadata.Labels[key]) + }) + } + componentGroupInfo := lo.MapEntries(byComponent, func(key string, value []*PodResponse) (string, int) { + return key, len(value) + }) + componentGroupsInfo := lo.Reject(lo.Entries(componentGroupInfo), func(item lo.Entry[string, int], index int) bool { + return item.Key == NoGroupKey + }) + byPartition = lo.OmitByKeys(byPartition, []string{NoGroupKey}) + partKeys := lo.Keys(byPartition) + sort.Strings(partKeys) + networkGroupsInfo := uniquePairs(partKeys) + + m.printPartitions(byComponent, "Component groups found") + m.printPartitions(byPartition, "Network groups found") + return byComponent, byComponent[NoGroupKey], componentGroupsInfo, networkGroupsInfo, nil +} + +func (m *Controller) hasNetworkExperiments() bool { + if m.cfg.Havoc.NetworkPartition != nil && m.cfg.Havoc.NetworkPartition.Label != "" { + return true + } + return false +} + +func (m *Controller) printPartitions(parts map[string][]*PodResponse, msg string) { + for _, p := range parts { + for _, pp := range p { + L.Info(). + Str("Name", pp.Metadata.Name). + Interface("Labels", pp.Metadata.Labels). + Msg(msg) + } + } +} + +// labelSelector transforms selector to ChaosMesh CRD format +func (m *Controller) labelSelector(k, v string) string { + if v == "" { + return NoGroupKey + } else { + return fmt.Sprintf("'%s': '%s'", k, v) + } +} + +// groupValueFromLabelSelector returns just the selector value +func (m *Controller) groupValueFromLabelSelector(selector string) string { + val := strings.Split(selector, ": ")[1] + return strings.ReplaceAll(val, "'", "") +} + +// GetPodsInfo gets info about all the pods in the namespace +func (m *Controller) GetPodsInfo(namespace string) (*PodsListResponse, error) { + if _, err := ExecCmd(fmt.Sprintf("kubectl get ns %s", namespace)); err != nil { + return nil, errors.Wrap(errors.New(ErrNoNamespace), namespace) + } + var cmdBuilder strings.Builder + cmdBuilder.Write([]byte(fmt.Sprintf("kubectl get pods -n %s ", namespace))) + if m.cfg.Havoc.NamespaceLabelFilter != "" { + cmdBuilder.Write([]byte(fmt.Sprintf("-l %s ", m.cfg.Havoc.NamespaceLabelFilter))) + } + cmdBuilder.Write([]byte("-o json")) + out, err := ExecCmd(cmdBuilder.String()) + if err != nil { + return nil, err + } + if err := dumpPodInfo(out); err != nil { + return nil, err + } + var pr *PodsListResponse + if err := json.Unmarshal([]byte(out), &pr); err != nil { + return nil, err + } + return pr, nil +} + +func dumpPodInfo(out string) error { + if L.GetLevel() == zerolog.DebugLevel { + var plr *PodsListResponse + if err := json.Unmarshal([]byte(out), &plr); err != nil { + return err + } + d, err := json.Marshal(plr) + if err != nil { + return err + } + _ = os.WriteFile("pods_dump.json", d, os.ModePerm) + return nil + } + return nil +} diff --git a/havoc/shell.nix b/havoc/shell.nix new file mode 100644 index 000000000..1eaee585a --- /dev/null +++ b/havoc/shell.nix @@ -0,0 +1,18 @@ +{ stdenv, pkgs, lib }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + go + gopls + delve + golangci-lint + gotools + jq + ]; + GOROOT="${pkgs.go}/share/go"; + + shellHook = '' + export PATH=$GOPATH/bin:$PATH + go install cmd/havoc.go + ''; +} diff --git a/havoc/testdata/configs/crib-all.toml b/havoc/testdata/configs/crib-all.toml new file mode 100644 index 000000000..c96ff1766 --- /dev/null +++ b/havoc/testdata/configs/crib-all.toml @@ -0,0 +1,126 @@ +[havoc] +# dir is a custom dir you can select, if null monkey will create a new dir +dir = "testdata/results/all" +# if you have multiple products inside one namespace this can help to filter by label in k=v format +namespace_label_filter = "" +# pods with this prefix will be ignored when generating experiments +ignore_pods = ["-db-"] +# name of the key to select components in the namespace +component_label_key = "havoc-component-group" +# group labels containing these strings will be ignored when generating group experiments +ignore_group_labels = [ + "mainnet", + "release", + "intents.otterize.com", + "pod-template-hash", + "rollouts-pod-template-hash", + "chain.link/app", + "chain.link/cost-center", + "chain.link/env", + "chain.link/project", + "chain.link/team", + "app.kubernetes.io/part-of", + "app.kubernetes.io/managed-by", + "app.chain.link/product", + "app.kubernetes.io/version", + "app.chain.link/blockchain", + "app.kubernetes.io/instance", + "app.kubernetes.io/name", +] +# these are experiment types you'd like to generate +experiment_types = [ + "external", + "failure", + "latency", + "cpu", + "memory", + "group-failure", + "group-latency", + "group-cpu", + "group-memory", + "group-partition", + "blockchain_rewind_head", +] +#experiment_types = ["group-partition"] + +[havoc.failure] +# duration of a "failure" experiment +duration = "10s" +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_fixed = ["3", "2", "1"] + +[havoc.latency] +# duration of "latency" experiment +duration = "10s" +# constant latency to inject +latency = "300ms" +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_fixed = ["3", "2", "1"] + +[havoc.stress_memory] +# duration of "stress" experiment affecting pod memory +duration = "10s" +# amount of workers which occupies memory +workers = 1 +# total amount of memory occupied +memory = "512MB" +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_fixed = ["3", "2", "1"] + +[havoc.stress_cpu] +# duration of "stress" experiment affecting pod CPU +duration = "10s" +# amount of workers which occupies cpu +workers = 1 +# amount of CPU core utilization, 100 means 1 worker will consume 1 cpu, 2 workers + 100 load = 2 CPUs +load = 100 +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_fixed = ["3", "2", "1"] + +[havoc.network_partition] +# duration of "network partition" experiment affecting pod CPU +duration = "30s" +# percentage of pods experiments affect in groups, see group-failure key and dir when generated +group_percentage = ["100"] +# a label to split pods for experiments +label = "havoc-network-group" + +[havoc.blockchain_rewind_head] +# duration of "blockchain" experiment +duration = "30s" + +[[havoc.blockchain_rewind_head.nodes]] +# label of executor pod +executor_pod_prefix = "geth-1337" +# executor container name +executor_container_name = "geth-network" +# blockchain node internal HTTP URL +node_internal_http_url = "geth-1337:8544" +# blocks to rewind from last +blocks = [30, 20, 10] + +[[havoc.blockchain_rewind_head.nodes]] +# label of executor pod +executor_pod_prefix = "geth-2337" +# executor container name +executor_container_name = "geth-network" +# blockchain node internal HTTP URL +node_internal_http_url = "geth-2337:8544" +# blocks to rewind from last +blocks = [30, 20, 10] + +[havoc.external_targets] +# duration of "external" experiment +duration = "10s" +# URL of external service that'd fail to resolve +urls = ["www.google.com"] + +[havoc.monkey] +# havoc monkey mode: +# seq - runs all experiments from all dirs sequentially one time +# rand - runs random experiments from all dirs +mode = "rand" +# duration of havoc monkey +duration = "3m" +# cooldown between experiments +cooldown = "10s" diff --git a/havoc/testdata/deployments/deployment_crib_1.json b/havoc/testdata/deployments/deployment_crib_1.json new file mode 100755 index 000000000..e868e385e --- /dev/null +++ b/havoc/testdata/deployments/deployment_crib_1.json @@ -0,0 +1,178 @@ +{ + "items": [ + { + "metadata": { + "name": "app-node-1-6bfbd56f65-brdtd", + "labels": { + "app": "app", + "instance": "node-1", + "intents.otterize.com/server": "app-node-1-cl-cluster-f3da74", + "network-partition-group": "network-group-2", + "pod-template-hash": "6bfbd56f65", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-1-db-dcd746f4c-2s9q7", + "labels": { + "app": "app-db", + "instance": "node-1-db", + "intents.otterize.com/server": "app-node-1-db-cl-cluster-162f92", + "pod-template-hash": "dcd746f4c", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-2-c989699d9-ng772", + "labels": { + "app": "app", + "instance": "node-2", + "intents.otterize.com/server": "app-node-2-cl-cluster-23bb66", + "network-partition-group": "network-group-2", + "pod-template-hash": "c989699d9", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-2-db-d668d649c-tnxkm", + "labels": { + "app": "app-db", + "instance": "node-2-db", + "intents.otterize.com/server": "app-node-2-db-cl-cluster-6999ba", + "pod-template-hash": "d668d649c", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-3-77c8fcf86f-r2l8x", + "labels": { + "app": "app", + "instance": "node-3", + "intents.otterize.com/server": "app-node-3-cl-cluster-04fa6d", + "network-partition-group": "network-group-1", + "pod-template-hash": "77c8fcf86f", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-3-db-69c7b7974c-9c2r6", + "labels": { + "app": "app-db", + "instance": "node-3-db", + "intents.otterize.com/server": "app-node-3-db-cl-cluster-31f364", + "pod-template-hash": "69c7b7974c", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-4-764b96b95c-gl6nn", + "labels": { + "app": "app", + "instance": "node-4", + "intents.otterize.com/server": "app-node-4-cl-cluster-816e93", + "network-partition-group": "network-group-1", + "pod-template-hash": "764b96b95c", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-4-db-58dcbd8c77-5hlbw", + "labels": { + "app": "app-db", + "instance": "node-4-db", + "intents.otterize.com/server": "app-node-4-db-cl-cluster-dd129f", + "pod-template-hash": "58dcbd8c77", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-5-6998f89557-925zr", + "labels": { + "app": "app", + "instance": "node-5", + "intents.otterize.com/server": "app-node-5-cl-cluster-a9916b", + "network-partition-group": "network-group-1", + "pod-template-hash": "6998f89557", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-5-db-77ff64c687-q4kph", + "labels": { + "app": "app-db", + "instance": "node-5-db", + "intents.otterize.com/server": "app-node-5-db-cl-cluster-a7c606", + "pod-template-hash": "77ff64c687", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-6-788f849997-862hp", + "labels": { + "app": "app", + "instance": "node-6", + "intents.otterize.com/server": "app-node-6-cl-cluster-6ab39a", + "network-partition-group": "network-group-1", + "pod-template-hash": "788f849997", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-6-db-58c5d55ccc-tfclz", + "labels": { + "app": "app-db", + "instance": "node-6-db", + "intents.otterize.com/server": "app-node-6-db-cl-cluster-cacf39", + "pod-template-hash": "58c5d55ccc", + "release": "app" + } + } + }, + { + "metadata": { + "name": "geth-55b64cdf59-rxmxt", + "labels": { + "app": "geth", + "intents.otterize.com/server": "geth-cl-cluster-331d92", + "network-partition-group": "network-group-3", + "pod-template-hash": "55b64cdf59", + "release": "app" + } + } + }, + { + "metadata": { + "name": "mockserver-7cb865999c-c8pcw", + "labels": { + "app": "mockserver", + "intents.otterize.com/server": "mockserver-cl-cluster-dedbac", + "network-partition-group": "network-group-4", + "pod-template-hash": "7cb865999c", + "release": "app" + } + } + } + ] +} diff --git a/havoc/testdata/deployments/deployment_crib_block_rewind.json b/havoc/testdata/deployments/deployment_crib_block_rewind.json new file mode 100755 index 000000000..a78d91ebf --- /dev/null +++ b/havoc/testdata/deployments/deployment_crib_block_rewind.json @@ -0,0 +1,219 @@ +{ + "items": [ + { + "metadata": { + "name": "app-node-1-bootstrap-5b47fb4dbc-msbzz", + "labels": { + "app": "app", + "instance": "node-1", + "intents.otterize.com/server": "app-node-1-bootstrap-cl-cluster-95bb1e", + "pod-template-hash": "5b47fb4dbc", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-1-db-6748d86b64-8hph5", + "labels": { + "app": "app-db", + "havoc-component-group": "db", + "havoc-network-group": "db", + "instance": "node-1-db", + "intents.otterize.com/server": "app-node-1-db-cl-cluster-162f92", + "pod-template-hash": "6748d86b64", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-2-596bb765d6-kph9d", + "labels": { + "app": "app", + "havoc-component-group": "node", + "havoc-network-group": "2", + "instance": "node-2", + "intents.otterize.com/server": "app-node-2-cl-cluster-23bb66", + "pod-template-hash": "596bb765d6", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-2-db-596cd6857d-6mqkd", + "labels": { + "app": "app-db", + "havoc-component-group": "db", + "havoc-network-group": "db", + "instance": "node-2-db", + "intents.otterize.com/server": "app-node-2-db-cl-cluster-6999ba", + "pod-template-hash": "596cd6857d", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-3-6f554cc8b6-g5vk9", + "labels": { + "app": "app", + "havoc-component-group": "node", + "havoc-network-group": "2", + "instance": "node-3", + "intents.otterize.com/server": "app-node-3-cl-cluster-04fa6d", + "pod-template-hash": "6f554cc8b6", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-3-db-bf4fcdd4-5w828", + "labels": { + "app": "app-db", + "havoc-component-group": "db", + "havoc-network-group": "db", + "instance": "node-3-db", + "intents.otterize.com/server": "app-node-3-db-cl-cluster-31f364", + "pod-template-hash": "bf4fcdd4", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-4-cf9977d9c-4gt28", + "labels": { + "app": "app", + "havoc-component-group": "node", + "havoc-network-group": "1", + "instance": "node-4", + "intents.otterize.com/server": "app-node-4-cl-cluster-816e93", + "pod-template-hash": "cf9977d9c", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-4-db-86b848b46b-crwxj", + "labels": { + "app": "app-db", + "havoc-component-group": "db", + "havoc-network-group": "db", + "instance": "node-4-db", + "intents.otterize.com/server": "app-node-4-db-cl-cluster-dd129f", + "pod-template-hash": "86b848b46b", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-5-d557ccf49-s2ffs", + "labels": { + "app": "app", + "havoc-component-group": "node", + "havoc-network-group": "1", + "instance": "node-5", + "intents.otterize.com/server": "app-node-5-cl-cluster-a9916b", + "pod-template-hash": "d557ccf49", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-5-db-56999b58d8-gg2ml", + "labels": { + "app": "app-db", + "havoc-component-group": "db", + "havoc-network-group": "db", + "instance": "node-5-db", + "intents.otterize.com/server": "app-node-5-db-cl-cluster-a7c606", + "pod-template-hash": "56999b58d8", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-6-5f964f4b9-wcbth", + "labels": { + "app": "app", + "havoc-component-group": "node", + "havoc-network-group": "1", + "instance": "node-6", + "intents.otterize.com/server": "app-node-6-cl-cluster-6ab39a", + "pod-template-hash": "5f964f4b9", + "release": "app" + } + } + }, + { + "metadata": { + "name": "app-node-6-db-757b5c49cd-5w796", + "labels": { + "app": "app-db", + "havoc-component-group": "db", + "havoc-network-group": "db", + "instance": "node-6-db", + "intents.otterize.com/server": "app-node-6-db-cl-cluster-cacf39", + "pod-template-hash": "757b5c49cd", + "release": "app" + } + } + }, + { + "metadata": { + "name": "geth-1337-7f7c9fb6c6-hzdhn", + "labels": { + "app": "geth", + "havoc-component-group": "blockchain", + "havoc-network-group": "blockchain", + "intents.otterize.com/server": "geth-cl-cluster-331d92", + "pod-template-hash": "7f7c9fb6c6", + "release": "app" + } + } + }, + { + "metadata": { + "name": "geth-2337-7f7c9fb6c6-hzdhn", + "labels": { + "app": "geth", + "havoc-component-group": "blockchain", + "havoc-network-group": "blockchain", + "intents.otterize.com/server": "geth-cl-cluster-331d92", + "pod-template-hash": "7f7c9fb6c6", + "release": "app" + } + } + }, + { + "metadata": { + "name": "mockserver-7cb865999c-qwdt9", + "labels": { + "app": "mockserver", + "intents.otterize.com/server": "mockserver-cl-cluster-dedbac", + "pod-template-hash": "7cb865999c", + "release": "app" + } + } + }, + { + "metadata": { + "name": "runner-64c589dd4b-qh4lj", + "labels": { + "app": "runner", + "instance": "runner-1", + "intents.otterize.com/server": "runner-cl-cluster-61468b", + "pod-template-hash": "64c589dd4b", + "release": "app" + } + } + } + ] +} diff --git a/havoc/testdata/deployments/deployment_single_group.json b/havoc/testdata/deployments/deployment_single_group.json new file mode 100755 index 000000000..9d59513e4 --- /dev/null +++ b/havoc/testdata/deployments/deployment_single_group.json @@ -0,0 +1,20 @@ +{ + "items": [ + { + "metadata": { + "name": "pod-1", + "labels": { + "havoc-component-group": "mygroup" + } + } + }, + { + "metadata": { + "name": "pod-2", + "labels": { + "havoc-component-group": "mygroup" + } + } + } + ] +} diff --git a/havoc/testdata/deployments/deployment_single_pod.json b/havoc/testdata/deployments/deployment_single_pod.json new file mode 100755 index 000000000..90df90fc8 --- /dev/null +++ b/havoc/testdata/deployments/deployment_single_pod.json @@ -0,0 +1,12 @@ +{ + "items": [ + { + "metadata": { + "name": "my-single-app", + "labels": { + "app": "app" + } + } + } + ] +} diff --git a/havoc/testdata/openapi_specs/petshop.yaml b/havoc/testdata/openapi_specs/petshop.yaml new file mode 100644 index 000000000..b01683dd6 --- /dev/null +++ b/havoc/testdata/openapi_specs/petshop.yaml @@ -0,0 +1,119 @@ +openapi: '3.0.0' +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + maximum: 100 + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pets' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: Create a pet + operationId: createPets + tags: + - pets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + maxItems: 100 + items: + $ref: '#/components/schemas/Pet' + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/havoc/testdata/results/.gitkeep b/havoc/testdata/results/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-10.yaml b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-10.yaml new file mode 100755 index 000000000..f40a20f49 --- /dev/null +++ b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-10.yaml @@ -0,0 +1,9 @@ +kind: blockchain_rewind_head +name: blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-10 +metadata: + name: blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-10 +podName: geth-1337-7f7c9fb6c6-hzdhn +executorContainerName: geth-network +nodeInternalHTTPURL: geth-1337:8544 +namespace: cl-cluster +blocks: 10 diff --git a/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-20.yaml b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-20.yaml new file mode 100755 index 000000000..8b1b36c57 --- /dev/null +++ b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-20.yaml @@ -0,0 +1,9 @@ +kind: blockchain_rewind_head +name: blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-20 +metadata: + name: blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-20 +podName: geth-1337-7f7c9fb6c6-hzdhn +executorContainerName: geth-network +nodeInternalHTTPURL: geth-1337:8544 +namespace: cl-cluster +blocks: 20 diff --git a/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-30.yaml b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-30.yaml new file mode 100755 index 000000000..6b23d4632 --- /dev/null +++ b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-30.yaml @@ -0,0 +1,9 @@ +kind: blockchain_rewind_head +name: blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-30 +metadata: + name: blockchain_rewind_head-geth-1337-7f7c9fb6c6-hzdhn-30 +podName: geth-1337-7f7c9fb6c6-hzdhn +executorContainerName: geth-network +nodeInternalHTTPURL: geth-1337:8544 +namespace: cl-cluster +blocks: 30 diff --git a/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-10.yaml b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-10.yaml new file mode 100755 index 000000000..7fa36ae49 --- /dev/null +++ b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-10.yaml @@ -0,0 +1,9 @@ +kind: blockchain_rewind_head +name: blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-10 +metadata: + name: blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-10 +podName: geth-2337-7f7c9fb6c6-hzdhn +executorContainerName: geth-network +nodeInternalHTTPURL: geth-2337:8544 +namespace: cl-cluster +blocks: 10 diff --git a/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-20.yaml b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-20.yaml new file mode 100755 index 000000000..fa2046953 --- /dev/null +++ b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-20.yaml @@ -0,0 +1,9 @@ +kind: blockchain_rewind_head +name: blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-20 +metadata: + name: blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-20 +podName: geth-2337-7f7c9fb6c6-hzdhn +executorContainerName: geth-network +nodeInternalHTTPURL: geth-2337:8544 +namespace: cl-cluster +blocks: 20 diff --git a/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-30.yaml b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-30.yaml new file mode 100755 index 000000000..0da844330 --- /dev/null +++ b/havoc/testdata/snapshot/all/blockchain_rewind_head/blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-30.yaml @@ -0,0 +1,9 @@ +kind: blockchain_rewind_head +name: blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-30 +metadata: + name: blockchain_rewind_head-geth-2337-7f7c9fb6c6-hzdhn-30 +podName: geth-2337-7f7c9fb6c6-hzdhn +executorContainerName: geth-network +nodeInternalHTTPURL: geth-2337:8544 +namespace: cl-cluster +blocks: 30 diff --git a/havoc/testdata/snapshot/all/cpu/cpu-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml b/havoc/testdata/snapshot/all/cpu/cpu-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml new file mode 100755 index 000000000..54c68929c --- /dev/null +++ b/havoc/testdata/snapshot/all/cpu/cpu-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml @@ -0,0 +1,15 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: cpu-app-node-1-bootstrap-5b47fb4dbc-msbzz + namespace: cl-cluster +spec: + mode: one + duration: 10s + selector: + fieldSelectors: + metadata.name: app-node-1-bootstrap-5b47fb4dbc-msbzz + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/all/cpu/cpu-mockserver-7cb865999c-qwdt9.yaml b/havoc/testdata/snapshot/all/cpu/cpu-mockserver-7cb865999c-qwdt9.yaml new file mode 100755 index 000000000..7bcd33890 --- /dev/null +++ b/havoc/testdata/snapshot/all/cpu/cpu-mockserver-7cb865999c-qwdt9.yaml @@ -0,0 +1,15 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: cpu-mockserver-7cb865999c-qwdt9 + namespace: cl-cluster +spec: + mode: one + duration: 10s + selector: + fieldSelectors: + metadata.name: mockserver-7cb865999c-qwdt9 + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/all/cpu/cpu-runner-64c589dd4b-qh4lj.yaml b/havoc/testdata/snapshot/all/cpu/cpu-runner-64c589dd4b-qh4lj.yaml new file mode 100755 index 000000000..bd8bccfca --- /dev/null +++ b/havoc/testdata/snapshot/all/cpu/cpu-runner-64c589dd4b-qh4lj.yaml @@ -0,0 +1,15 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: cpu-runner-64c589dd4b-qh4lj + namespace: cl-cluster +spec: + mode: one + duration: 10s + selector: + fieldSelectors: + metadata.name: runner-64c589dd4b-qh4lj + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/all/external/external-cl-cluster-0a137b375cc3881a70e186ce2172c8d1.yaml b/havoc/testdata/snapshot/all/external/external-cl-cluster-0a137b375cc3881a70e186ce2172c8d1.yaml new file mode 100755 index 000000000..c2d326f79 --- /dev/null +++ b/havoc/testdata/snapshot/all/external/external-cl-cluster-0a137b375cc3881a70e186ce2172c8d1.yaml @@ -0,0 +1,20 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: external-cl-cluster-0a137b375cc3881a70e186ce2172c8d1 + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + mode: all + action: partition + duration: 10s + direction: to + target: + selector: + namespaces: + - cl-cluster + mode: all + externalTargets: + - 'www.google.com' diff --git a/havoc/testdata/snapshot/all/failure/failure-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml b/havoc/testdata/snapshot/all/failure/failure-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml new file mode 100755 index 000000000..4277a7821 --- /dev/null +++ b/havoc/testdata/snapshot/all/failure/failure-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml @@ -0,0 +1,12 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: failure-app-node-1-bootstrap-5b47fb4dbc-msbzz + namespace: cl-cluster +spec: + action: pod-failure + mode: one + duration: 10s + selector: + fieldSelectors: + metadata.name: app-node-1-bootstrap-5b47fb4dbc-msbzz diff --git a/havoc/testdata/snapshot/all/failure/failure-mockserver-7cb865999c-qwdt9.yaml b/havoc/testdata/snapshot/all/failure/failure-mockserver-7cb865999c-qwdt9.yaml new file mode 100755 index 000000000..193905cec --- /dev/null +++ b/havoc/testdata/snapshot/all/failure/failure-mockserver-7cb865999c-qwdt9.yaml @@ -0,0 +1,12 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: failure-mockserver-7cb865999c-qwdt9 + namespace: cl-cluster +spec: + action: pod-failure + mode: one + duration: 10s + selector: + fieldSelectors: + metadata.name: mockserver-7cb865999c-qwdt9 diff --git a/havoc/testdata/snapshot/all/failure/failure-runner-64c589dd4b-qh4lj.yaml b/havoc/testdata/snapshot/all/failure/failure-runner-64c589dd4b-qh4lj.yaml new file mode 100755 index 000000000..1b14d7a63 --- /dev/null +++ b/havoc/testdata/snapshot/all/failure/failure-runner-64c589dd4b-qh4lj.yaml @@ -0,0 +1,12 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: failure-runner-64c589dd4b-qh4lj + namespace: cl-cluster +spec: + action: pod-failure + mode: one + duration: 10s + selector: + fieldSelectors: + metadata.name: runner-64c589dd4b-qh4lj diff --git a/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-1-fixed.yaml b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-1-fixed.yaml new file mode 100755 index 000000000..6f19dace3 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-1-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-cpu-havoc-component-group-blockchain-1-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '1' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'blockchain' + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-2-fixed.yaml b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-2-fixed.yaml new file mode 100755 index 000000000..dda864472 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-2-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-cpu-havoc-component-group-blockchain-2-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '2' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'blockchain' + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-3-fixed.yaml b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-3-fixed.yaml new file mode 100755 index 000000000..672eddbb6 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-blockchain-3-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-cpu-havoc-component-group-blockchain-3-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '3' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'blockchain' + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-1-fixed.yaml b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-1-fixed.yaml new file mode 100755 index 000000000..8d829a849 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-1-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-cpu-havoc-component-group-node-1-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '1' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'node' + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-2-fixed.yaml b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-2-fixed.yaml new file mode 100755 index 000000000..c5a513aa8 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-2-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-cpu-havoc-component-group-node-2-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '2' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'node' + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-3-fixed.yaml b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-3-fixed.yaml new file mode 100755 index 000000000..ddfde5ea7 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-cpu/group-cpu-havoc-component-group-node-3-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-cpu-havoc-component-group-node-3-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '3' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'node' + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-1-fixed.yaml b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-1-fixed.yaml new file mode 100755 index 000000000..a0bf4b7bc --- /dev/null +++ b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-1-fixed.yaml @@ -0,0 +1,13 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: group-failure-havoc-component-group-blockchain-1-fixed + namespace: cl-cluster +spec: + action: pod-failure + mode: fixed + value: '1' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'blockchain' diff --git a/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-2-fixed.yaml b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-2-fixed.yaml new file mode 100755 index 000000000..069489995 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-2-fixed.yaml @@ -0,0 +1,13 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: group-failure-havoc-component-group-blockchain-2-fixed + namespace: cl-cluster +spec: + action: pod-failure + mode: fixed + value: '2' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'blockchain' diff --git a/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-3-fixed.yaml b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-3-fixed.yaml new file mode 100755 index 000000000..d9d783550 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-blockchain-3-fixed.yaml @@ -0,0 +1,13 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: group-failure-havoc-component-group-blockchain-3-fixed + namespace: cl-cluster +spec: + action: pod-failure + mode: fixed + value: '3' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'blockchain' diff --git a/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-1-fixed.yaml b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-1-fixed.yaml new file mode 100755 index 000000000..374d31385 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-1-fixed.yaml @@ -0,0 +1,13 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: group-failure-havoc-component-group-node-1-fixed + namespace: cl-cluster +spec: + action: pod-failure + mode: fixed + value: '1' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'node' diff --git a/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-2-fixed.yaml b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-2-fixed.yaml new file mode 100755 index 000000000..be3310deb --- /dev/null +++ b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-2-fixed.yaml @@ -0,0 +1,13 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: group-failure-havoc-component-group-node-2-fixed + namespace: cl-cluster +spec: + action: pod-failure + mode: fixed + value: '2' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'node' diff --git a/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-3-fixed.yaml b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-3-fixed.yaml new file mode 100755 index 000000000..e1242efcf --- /dev/null +++ b/havoc/testdata/snapshot/all/group-failure/group-failure-havoc-component-group-node-3-fixed.yaml @@ -0,0 +1,13 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: group-failure-havoc-component-group-node-3-fixed + namespace: cl-cluster +spec: + action: pod-failure + mode: fixed + value: '3' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'node' diff --git a/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-1-fixed.yaml b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-1-fixed.yaml new file mode 100755 index 000000000..3ec71dcdf --- /dev/null +++ b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-1-fixed.yaml @@ -0,0 +1,26 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-latency-havoc-component-group-blockchain-1-fixed + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'blockchain' + mode: fixed + value: '1' + action: delay + duration: 10s + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'blockchain' + mode: fixed + value: '1' diff --git a/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-2-fixed.yaml b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-2-fixed.yaml new file mode 100755 index 000000000..c3bac3f3d --- /dev/null +++ b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-2-fixed.yaml @@ -0,0 +1,26 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-latency-havoc-component-group-blockchain-2-fixed + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'blockchain' + mode: fixed + value: '2' + action: delay + duration: 10s + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'blockchain' + mode: fixed + value: '2' diff --git a/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-3-fixed.yaml b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-3-fixed.yaml new file mode 100755 index 000000000..667dee522 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-blockchain-3-fixed.yaml @@ -0,0 +1,26 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-latency-havoc-component-group-blockchain-3-fixed + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'blockchain' + mode: fixed + value: '3' + action: delay + duration: 10s + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'blockchain' + mode: fixed + value: '3' diff --git a/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-1-fixed.yaml b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-1-fixed.yaml new file mode 100755 index 000000000..46ad46dc2 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-1-fixed.yaml @@ -0,0 +1,26 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-latency-havoc-component-group-node-1-fixed + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'node' + mode: fixed + value: '1' + action: delay + duration: 10s + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'node' + mode: fixed + value: '1' diff --git a/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-2-fixed.yaml b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-2-fixed.yaml new file mode 100755 index 000000000..3fee88064 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-2-fixed.yaml @@ -0,0 +1,26 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-latency-havoc-component-group-node-2-fixed + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'node' + mode: fixed + value: '2' + action: delay + duration: 10s + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'node' + mode: fixed + value: '2' diff --git a/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-3-fixed.yaml b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-3-fixed.yaml new file mode 100755 index 000000000..e6e2d3fed --- /dev/null +++ b/havoc/testdata/snapshot/all/group-latency/group-latency-havoc-component-group-node-3-fixed.yaml @@ -0,0 +1,26 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-latency-havoc-component-group-node-3-fixed + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'node' + mode: fixed + value: '3' + action: delay + duration: 10s + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'node' + mode: fixed + value: '3' diff --git a/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-1-fixed.yaml b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-1-fixed.yaml new file mode 100755 index 000000000..7258e81cc --- /dev/null +++ b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-1-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-memory-havoc-component-group-blockchain-1-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '1' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'blockchain' + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-2-fixed.yaml b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-2-fixed.yaml new file mode 100755 index 000000000..7224938f4 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-2-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-memory-havoc-component-group-blockchain-2-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '2' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'blockchain' + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-3-fixed.yaml b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-3-fixed.yaml new file mode 100755 index 000000000..f1b20f768 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-blockchain-3-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-memory-havoc-component-group-blockchain-3-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '3' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'blockchain' + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-1-fixed.yaml b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-1-fixed.yaml new file mode 100755 index 000000000..f524039b5 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-1-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-memory-havoc-component-group-node-1-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '1' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'node' + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-2-fixed.yaml b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-2-fixed.yaml new file mode 100755 index 000000000..59a5e3ec2 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-2-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-memory-havoc-component-group-node-2-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '2' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'node' + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-3-fixed.yaml b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-3-fixed.yaml new file mode 100755 index 000000000..fc4cbd4b9 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-memory/group-memory-havoc-component-group-node-3-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-memory-havoc-component-group-node-3-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '3' + duration: 10s + selector: + labelSelectors: + 'havoc-component-group': 'node' + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-1-to-havoc-network-group-2-100-perc.yaml b/havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-1-to-havoc-network-group-2-100-perc.yaml new file mode 100755 index 000000000..80e7f2d80 --- /dev/null +++ b/havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-1-to-havoc-network-group-2-100-perc.yaml @@ -0,0 +1,24 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-partition-havoc-network-group-1-to-havoc-network-group-2-100-perc + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-network-group': '1' + action: partition + mode: fixed-percent + value: '100' + duration: 30s + direction: from + target: + mode: fixed-percent + value: '100' + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-network-group': '2' diff --git a/havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-1-to-havoc-network-group-blockchain-100-perc.yaml b/havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-1-to-havoc-network-group-blockchain-100-perc.yaml new file mode 100755 index 000000000..90aa573aa --- /dev/null +++ b/havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-1-to-havoc-network-group-blockchain-100-perc.yaml @@ -0,0 +1,24 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-partition-havoc-network-group-1-to-havoc-network-group-blockchain-100-perc + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-network-group': '1' + action: partition + mode: fixed-percent + value: '100' + duration: 30s + direction: from + target: + mode: fixed-percent + value: '100' + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-network-group': 'blockchain' diff --git a/havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-2-to-havoc-network-group-blockchain-100-perc.yaml b/havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-2-to-havoc-network-group-blockchain-100-perc.yaml new file mode 100755 index 000000000..b5698d0ae --- /dev/null +++ b/havoc/testdata/snapshot/all/group-partition/group-partition-havoc-network-group-2-to-havoc-network-group-blockchain-100-perc.yaml @@ -0,0 +1,24 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-partition-havoc-network-group-2-to-havoc-network-group-blockchain-100-perc + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-network-group': '2' + action: partition + mode: fixed-percent + value: '100' + duration: 30s + direction: from + target: + mode: fixed-percent + value: '100' + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-network-group': 'blockchain' diff --git a/havoc/testdata/snapshot/all/latency/latency-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml b/havoc/testdata/snapshot/all/latency/latency-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml new file mode 100755 index 000000000..070ea19a0 --- /dev/null +++ b/havoc/testdata/snapshot/all/latency/latency-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml @@ -0,0 +1,24 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: latency-app-node-1-bootstrap-5b47fb4dbc-msbzz + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + fieldSelectors: + metadata.name: app-node-1-bootstrap-5b47fb4dbc-msbzz + mode: one + action: delay + duration: 10s + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + fieldSelectors: + metadata.name: app-node-1-bootstrap-5b47fb4dbc-msbzz + mode: one diff --git a/havoc/testdata/snapshot/all/latency/latency-mockserver-7cb865999c-qwdt9.yaml b/havoc/testdata/snapshot/all/latency/latency-mockserver-7cb865999c-qwdt9.yaml new file mode 100755 index 000000000..1e9b31c76 --- /dev/null +++ b/havoc/testdata/snapshot/all/latency/latency-mockserver-7cb865999c-qwdt9.yaml @@ -0,0 +1,24 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: latency-mockserver-7cb865999c-qwdt9 + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + fieldSelectors: + metadata.name: mockserver-7cb865999c-qwdt9 + mode: one + action: delay + duration: 10s + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + fieldSelectors: + metadata.name: mockserver-7cb865999c-qwdt9 + mode: one diff --git a/havoc/testdata/snapshot/all/latency/latency-runner-64c589dd4b-qh4lj.yaml b/havoc/testdata/snapshot/all/latency/latency-runner-64c589dd4b-qh4lj.yaml new file mode 100755 index 000000000..b5d5525b4 --- /dev/null +++ b/havoc/testdata/snapshot/all/latency/latency-runner-64c589dd4b-qh4lj.yaml @@ -0,0 +1,24 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: latency-runner-64c589dd4b-qh4lj + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + fieldSelectors: + metadata.name: runner-64c589dd4b-qh4lj + mode: one + action: delay + duration: 10s + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + fieldSelectors: + metadata.name: runner-64c589dd4b-qh4lj + mode: one diff --git a/havoc/testdata/snapshot/all/memory/memory-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml b/havoc/testdata/snapshot/all/memory/memory-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml new file mode 100755 index 000000000..fed593cdd --- /dev/null +++ b/havoc/testdata/snapshot/all/memory/memory-app-node-1-bootstrap-5b47fb4dbc-msbzz.yaml @@ -0,0 +1,15 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: memory-app-node-1-bootstrap-5b47fb4dbc-msbzz + namespace: cl-cluster +spec: + mode: one + duration: 10s + selector: + fieldSelectors: + metadata.name: app-node-1-bootstrap-5b47fb4dbc-msbzz + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/all/memory/memory-mockserver-7cb865999c-qwdt9.yaml b/havoc/testdata/snapshot/all/memory/memory-mockserver-7cb865999c-qwdt9.yaml new file mode 100755 index 000000000..4c4c6fa26 --- /dev/null +++ b/havoc/testdata/snapshot/all/memory/memory-mockserver-7cb865999c-qwdt9.yaml @@ -0,0 +1,15 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: memory-mockserver-7cb865999c-qwdt9 + namespace: cl-cluster +spec: + mode: one + duration: 10s + selector: + fieldSelectors: + metadata.name: mockserver-7cb865999c-qwdt9 + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/all/memory/memory-runner-64c589dd4b-qh4lj.yaml b/havoc/testdata/snapshot/all/memory/memory-runner-64c589dd4b-qh4lj.yaml new file mode 100755 index 000000000..af7940d7a --- /dev/null +++ b/havoc/testdata/snapshot/all/memory/memory-runner-64c589dd4b-qh4lj.yaml @@ -0,0 +1,15 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: memory-runner-64c589dd4b-qh4lj + namespace: cl-cluster +spec: + mode: one + duration: 10s + selector: + fieldSelectors: + metadata.name: runner-64c589dd4b-qh4lj + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-1-fixed.yaml b/havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-1-fixed.yaml new file mode 100755 index 000000000..48239cd35 --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-1-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-cpu-havoc-component-group-mygroup-1-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '1' + duration: 1m + selector: + labelSelectors: + 'havoc-component-group': 'mygroup' + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-2-fixed.yaml b/havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-2-fixed.yaml new file mode 100755 index 000000000..7f4a4e0d9 --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-2-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-cpu-havoc-component-group-mygroup-2-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '2' + duration: 1m + selector: + labelSelectors: + 'havoc-component-group': 'mygroup' + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-3-fixed.yaml b/havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-3-fixed.yaml new file mode 100755 index 000000000..86754957d --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-cpu/group-cpu-havoc-component-group-mygroup-3-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-cpu-havoc-component-group-mygroup-3-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '3' + duration: 1m + selector: + labelSelectors: + 'havoc-component-group': 'mygroup' + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-1-fixed.yaml b/havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-1-fixed.yaml new file mode 100755 index 000000000..5eca317b5 --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-1-fixed.yaml @@ -0,0 +1,13 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: group-failure-havoc-component-group-mygroup-1-fixed + namespace: cl-cluster +spec: + action: pod-failure + mode: fixed + value: '1' + duration: 1m + selector: + labelSelectors: + 'havoc-component-group': 'mygroup' diff --git a/havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-2-fixed.yaml b/havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-2-fixed.yaml new file mode 100755 index 000000000..0e3a9d4f1 --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-2-fixed.yaml @@ -0,0 +1,13 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: group-failure-havoc-component-group-mygroup-2-fixed + namespace: cl-cluster +spec: + action: pod-failure + mode: fixed + value: '2' + duration: 1m + selector: + labelSelectors: + 'havoc-component-group': 'mygroup' diff --git a/havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-3-fixed.yaml b/havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-3-fixed.yaml new file mode 100755 index 000000000..58a8dc3e1 --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-failure/group-failure-havoc-component-group-mygroup-3-fixed.yaml @@ -0,0 +1,13 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: group-failure-havoc-component-group-mygroup-3-fixed + namespace: cl-cluster +spec: + action: pod-failure + mode: fixed + value: '3' + duration: 1m + selector: + labelSelectors: + 'havoc-component-group': 'mygroup' diff --git a/havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-1-fixed.yaml b/havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-1-fixed.yaml new file mode 100755 index 000000000..12acb9541 --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-1-fixed.yaml @@ -0,0 +1,26 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-latency-havoc-component-group-mygroup-1-fixed + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'mygroup' + mode: fixed + value: '1' + action: delay + duration: 1m + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'mygroup' + mode: fixed + value: '1' diff --git a/havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-2-fixed.yaml b/havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-2-fixed.yaml new file mode 100755 index 000000000..fd59b1d53 --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-2-fixed.yaml @@ -0,0 +1,26 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-latency-havoc-component-group-mygroup-2-fixed + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'mygroup' + mode: fixed + value: '2' + action: delay + duration: 1m + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'mygroup' + mode: fixed + value: '2' diff --git a/havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-3-fixed.yaml b/havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-3-fixed.yaml new file mode 100755 index 000000000..985c9280b --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-latency/group-latency-havoc-component-group-mygroup-3-fixed.yaml @@ -0,0 +1,26 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: group-latency-havoc-component-group-mygroup-3-fixed + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'mygroup' + mode: fixed + value: '3' + action: delay + duration: 1m + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + labelSelectors: + 'havoc-component-group': 'mygroup' + mode: fixed + value: '3' diff --git a/havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-1-fixed.yaml b/havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-1-fixed.yaml new file mode 100755 index 000000000..4950c703c --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-1-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-memory-havoc-component-group-mygroup-1-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '1' + duration: 1m + selector: + labelSelectors: + 'havoc-component-group': 'mygroup' + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-2-fixed.yaml b/havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-2-fixed.yaml new file mode 100755 index 000000000..f1ada38be --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-2-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-memory-havoc-component-group-mygroup-2-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '2' + duration: 1m + selector: + labelSelectors: + 'havoc-component-group': 'mygroup' + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-3-fixed.yaml b/havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-3-fixed.yaml new file mode 100755 index 000000000..5fe9602f5 --- /dev/null +++ b/havoc/testdata/snapshot/single_group/group-memory/group-memory-havoc-component-group-mygroup-3-fixed.yaml @@ -0,0 +1,16 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: group-memory-havoc-component-group-mygroup-3-fixed + namespace: cl-cluster +spec: + mode: fixed + value: '3' + duration: 1m + selector: + labelSelectors: + 'havoc-component-group': 'mygroup' + stressors: + memory: + workers: 1 + size: 512MB diff --git a/havoc/testdata/snapshot/single_pod/cpu/cpu-my-single-app.yaml b/havoc/testdata/snapshot/single_pod/cpu/cpu-my-single-app.yaml new file mode 100755 index 000000000..8acde5612 --- /dev/null +++ b/havoc/testdata/snapshot/single_pod/cpu/cpu-my-single-app.yaml @@ -0,0 +1,15 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: cpu-my-single-app + namespace: cl-cluster +spec: + mode: one + duration: 1m + selector: + fieldSelectors: + metadata.name: my-single-app + stressors: + cpu: + workers: 1 + load: 100 diff --git a/havoc/testdata/snapshot/single_pod/failure/failure-my-single-app.yaml b/havoc/testdata/snapshot/single_pod/failure/failure-my-single-app.yaml new file mode 100755 index 000000000..30cb81596 --- /dev/null +++ b/havoc/testdata/snapshot/single_pod/failure/failure-my-single-app.yaml @@ -0,0 +1,12 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: PodChaos +metadata: + name: failure-my-single-app + namespace: cl-cluster +spec: + action: pod-failure + mode: one + duration: 1m + selector: + fieldSelectors: + metadata.name: my-single-app diff --git a/havoc/testdata/snapshot/single_pod/latency/latency-my-single-app.yaml b/havoc/testdata/snapshot/single_pod/latency/latency-my-single-app.yaml new file mode 100755 index 000000000..f197dbf62 --- /dev/null +++ b/havoc/testdata/snapshot/single_pod/latency/latency-my-single-app.yaml @@ -0,0 +1,24 @@ +kind: NetworkChaos +apiVersion: chaos-mesh.org/v1alpha1 +metadata: + name: latency-my-single-app + namespace: cl-cluster +spec: + selector: + namespaces: + - cl-cluster + fieldSelectors: + metadata.name: my-single-app + mode: one + action: delay + duration: 1m + delay: + latency: 300ms + direction: from + target: + selector: + namespaces: + - cl-cluster + fieldSelectors: + metadata.name: my-single-app + mode: one diff --git a/havoc/testdata/snapshot/single_pod/memory/memory-my-single-app.yaml b/havoc/testdata/snapshot/single_pod/memory/memory-my-single-app.yaml new file mode 100755 index 000000000..c842fe487 --- /dev/null +++ b/havoc/testdata/snapshot/single_pod/memory/memory-my-single-app.yaml @@ -0,0 +1,15 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: StressChaos +metadata: + name: memory-my-single-app + namespace: cl-cluster +spec: + mode: one + duration: 1m + selector: + fieldSelectors: + metadata.name: my-single-app + stressors: + memory: + workers: 1 + size: 512MB