mirror of
https://github.com/k8sgpt-ai/k8sgpt.git
synced 2026-03-18 19:17:25 +00:00
Compare commits
372 Commits
v0.3.23
...
feat/bedro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d5c08c615 | ||
|
|
83672fa768 | ||
|
|
990d723909 | ||
|
|
c506a4b441 | ||
|
|
2918556793 | ||
|
|
19abbef9a3 | ||
|
|
939e0672aa | ||
|
|
8cd3b2985e | ||
|
|
1827c43696 | ||
|
|
1363219b1b | ||
|
|
c3f448693d | ||
|
|
5514ebb53b | ||
|
|
c21ba86237 | ||
|
|
d6d80ee860 | ||
|
|
a841568a9c | ||
|
|
4d7eb0f622 | ||
|
|
a12aa07b1a | ||
|
|
ec5e42b8f4 | ||
|
|
69c67bd1d9 | ||
|
|
b3f60b2d20 | ||
|
|
cb1e1ffede | ||
|
|
f37d923918 | ||
|
|
a50375c960 | ||
|
|
da266b3c82 | ||
|
|
896a53be83 | ||
|
|
2da057360b | ||
|
|
69fd7c7696 | ||
|
|
ad86e7aa39 | ||
|
|
1ae70e806e | ||
|
|
2ce8450e03 | ||
|
|
b6b3d0c856 | ||
|
|
5f7d9de46a | ||
|
|
7dcdfc83d2 | ||
|
|
44f4f7437f | ||
|
|
783cd1cfc6 | ||
|
|
62ba6d84a4 | ||
|
|
cc9b3ea657 | ||
|
|
8ad288152f | ||
|
|
004d9efc55 | ||
|
|
2f62c7b3e0 | ||
|
|
a6b19a81b0 | ||
|
|
32e8bc3253 | ||
|
|
5f01cc685f | ||
|
|
c67add30c6 | ||
|
|
2f759865b6 | ||
|
|
9d68c47040 | ||
|
|
14e0f19b12 | ||
|
|
c1a38c2b35 | ||
|
|
458fcfe8d3 | ||
|
|
173e4dc5ac | ||
|
|
7a3fb3cf67 | ||
|
|
0cfecbdd87 | ||
|
|
161bc11294 | ||
|
|
ad349ae263 | ||
|
|
911d578bf0 | ||
|
|
3eec9bbb05 | ||
|
|
87565a0bcc | ||
|
|
c128bf7942 | ||
|
|
c9b11b6eee | ||
|
|
9f39abf89e | ||
|
|
98237b6408 | ||
|
|
4143e9fd52 | ||
|
|
72eb8159fb | ||
|
|
36135857ac | ||
|
|
9c1927b497 | ||
|
|
0148a5b354 | ||
|
|
037e745c6f | ||
|
|
d8fad956f4 | ||
|
|
7785dd12a0 | ||
|
|
04582d8516 | ||
|
|
da0764d951 | ||
|
|
b62b7dbe3c | ||
|
|
5ff6dc9be5 | ||
|
|
f071b32aa8 | ||
|
|
53465d5c83 | ||
|
|
1dfd139731 | ||
|
|
d9c81494b7 | ||
|
|
e0e86ea60f | ||
|
|
8405778cb2 | ||
|
|
4f3ecf0083 | ||
|
|
d30563d8cd | ||
|
|
e02c0ddd2d | ||
|
|
025a069ff1 | ||
|
|
453d5c37dd | ||
|
|
be4ca86af0 | ||
|
|
477ef155d3 | ||
|
|
8edb053b3e | ||
|
|
644581f495 | ||
|
|
d702209941 | ||
|
|
7019d0b62f | ||
|
|
f57381961f | ||
|
|
02fa109429 | ||
|
|
3148b5c61d | ||
|
|
a4e44d59e3 | ||
|
|
24ebeaf3a7 | ||
|
|
db26d24ac6 | ||
|
|
b2b86826e5 | ||
|
|
8e37369e5c | ||
|
|
2a8a9b4867 | ||
|
|
b7e5394caa | ||
|
|
ad117a530f | ||
|
|
16d57e5a55 | ||
|
|
3547c4808a | ||
|
|
407c855e14 | ||
|
|
d43fd878ba | ||
|
|
a068310731 | ||
|
|
8949f5bac3 | ||
|
|
f1b7b37fb8 | ||
|
|
391a3cd5ad | ||
|
|
3c08118104 | ||
|
|
eb3b81f176 | ||
|
|
5176759bd0 | ||
|
|
4b13727ef5 | ||
|
|
f9edbf34f3 | ||
|
|
1a00aafbb2 | ||
|
|
b6dd2a1181 | ||
|
|
4366ad97b8 | ||
|
|
34b6de3404 | ||
|
|
de9ef85878 | ||
|
|
0b906511d5 | ||
|
|
4d76e9c5ae | ||
|
|
593139cffb | ||
|
|
3e9340925c | ||
|
|
f6ce47c3a9 | ||
|
|
02e754ed59 | ||
|
|
fef853966f | ||
|
|
dd20dbc982 | ||
|
|
dd66355797 | ||
|
|
314f25ac8b | ||
|
|
d4abb33b3c | ||
|
|
27ac60aed2 | ||
|
|
0c0216096e | ||
|
|
b35dbd9b09 | ||
|
|
a075792119 | ||
|
|
ce63821beb | ||
|
|
ab534d184f | ||
|
|
3f80bbaa1b | ||
|
|
9bace02a67 | ||
|
|
7b1b63322e | ||
|
|
f963e4e0f4 | ||
|
|
2382de4c6f | ||
|
|
55ae7c3298 | ||
|
|
aeae2ba765 | ||
|
|
602d111d85 | ||
|
|
c3f164eb2b | ||
|
|
92dd1bd8b0 | ||
|
|
4867d39c66 | ||
|
|
c834c09996 | ||
|
|
038e52e044 | ||
|
|
f9d734ee53 | ||
|
|
c101e8a3ea | ||
|
|
63b63f7664 | ||
|
|
0fe984966a | ||
|
|
3a893184af | ||
|
|
6652fbe7cb | ||
|
|
728555c0ef | ||
|
|
bdd470f9ca | ||
|
|
fad00eac49 | ||
|
|
3452c0def6 | ||
|
|
ffd017fbd7 | ||
|
|
e261c09889 | ||
|
|
cc890dfa46 | ||
|
|
047afd46d6 | ||
|
|
eda52312ae | ||
|
|
882c6f5225 | ||
|
|
fe53907c44 | ||
|
|
63f7fcfef7 | ||
|
|
2c7c74472c | ||
|
|
3c4823127c | ||
|
|
c3a884f0c4 | ||
|
|
e74fc0838f | ||
|
|
f30c9f5554 | ||
|
|
36ccc62846 | ||
|
|
a809a455f5 | ||
|
|
12fa5aef4d | ||
|
|
75c2addf66 | ||
|
|
9b797d7e8b | ||
|
|
820cd2e16c | ||
|
|
43953ffa34 | ||
|
|
bd695d0987 | ||
|
|
b12c006c63 | ||
|
|
e0af76f3c9 | ||
|
|
e894e778e9 | ||
|
|
5cfe3325cb | ||
|
|
ea8183ce84 | ||
|
|
24cff90a0c | ||
|
|
bf6f642c28 | ||
|
|
6279f358ca | ||
|
|
b2ab94375e | ||
|
|
9a73d1923f | ||
|
|
85a76a3be0 | ||
|
|
aa276a5379 | ||
|
|
eb7687a089 | ||
|
|
c162cc22ee | ||
|
|
1ae4e75196 | ||
|
|
693b23f1fc | ||
|
|
e6085d4191 | ||
|
|
3eaf776249 | ||
|
|
ccb692c1fd | ||
|
|
9e0263778f | ||
|
|
9dfcce842e | ||
|
|
6df0169491 | ||
|
|
007b4bb8ec | ||
|
|
6b38a56afb | ||
|
|
19ae31b5dd | ||
|
|
6a46a26789 | ||
|
|
e05a902d90 | ||
|
|
a3896f4518 | ||
|
|
4262c9292c | ||
|
|
94cdce44b4 | ||
|
|
10c00ba9fe | ||
|
|
c872e495ad | ||
|
|
bd2e06bae7 | ||
|
|
5db4bc28a7 | ||
|
|
8f8f5c6df7 | ||
|
|
3c1c055ac7 | ||
|
|
ebfbba98ca | ||
|
|
47463d4412 | ||
|
|
fe81d16f75 | ||
|
|
a1d0d0a180 | ||
|
|
f60467cd4d | ||
|
|
20892b48d0 | ||
|
|
ea7f0a5b4e | ||
|
|
531f0bc46d | ||
|
|
28e19a9d4e | ||
|
|
3475e2de0c | ||
|
|
adf4f17085 | ||
|
|
55ac0b2129 | ||
|
|
a0225d4f70 | ||
|
|
b05b6a38ed | ||
|
|
1340ead860 | ||
|
|
b58b7191af | ||
|
|
1491e67567 | ||
|
|
4ec143ab77 | ||
|
|
5199dadb2a | ||
|
|
425f33bb2d | ||
|
|
f5c3f18d87 | ||
|
|
d2754d320f | ||
|
|
85f18dde1f | ||
|
|
16a4aaab81 | ||
|
|
4065faef13 | ||
|
|
f24bcd88b6 | ||
|
|
307710eddc | ||
|
|
aab8d77feb | ||
|
|
334a86aaf4 | ||
|
|
88a7907db4 | ||
|
|
af3732ad06 | ||
|
|
a81377f72d | ||
|
|
6103c96c41 | ||
|
|
35f5185914 | ||
|
|
97446aae07 | ||
|
|
e07822c10b | ||
|
|
f929e7feea | ||
|
|
6e640e6921 | ||
|
|
98286a965e | ||
|
|
6ac815c10f | ||
|
|
8f00218090 | ||
|
|
00c91f05a6 | ||
|
|
6207c70c51 | ||
|
|
8b0b61e596 | ||
|
|
248260e081 | ||
|
|
f55f8370eb | ||
|
|
a3cd7e6385 | ||
|
|
f2138c7101 | ||
|
|
3f0356be66 | ||
|
|
cc99bd51f0 | ||
|
|
729d14db4d | ||
|
|
fea2ed1fff | ||
|
|
c8c9dbfadc | ||
|
|
070aa7fdd0 | ||
|
|
ce7c9551bc | ||
|
|
d9fe7446af | ||
|
|
9c1f1b8804 | ||
|
|
37228d88e3 | ||
|
|
29b482f597 | ||
|
|
015bccfc2e | ||
|
|
3f0964ad38 | ||
|
|
3c8d9d42e5 | ||
|
|
bfbb5c7e03 | ||
|
|
28c4c57e45 | ||
|
|
4e57088a01 | ||
|
|
f2eb1ef533 | ||
|
|
bbf61f53d4 | ||
|
|
3d2554b9cd | ||
|
|
f61c3e228c | ||
|
|
c6019728ae | ||
|
|
e3eee6d956 | ||
|
|
599be33f38 | ||
|
|
3415031006 | ||
|
|
d97dea2896 | ||
|
|
f9c1b90338 | ||
|
|
78126b2328 | ||
|
|
60853fe4eb | ||
|
|
a253af23b6 | ||
|
|
2fd476e126 | ||
|
|
483a9dad10 | ||
|
|
817d9cf754 | ||
|
|
72e08efff1 | ||
|
|
e7d690afd1 | ||
|
|
3cf18e783e | ||
|
|
cdbeb146a2 | ||
|
|
2effbb345a | ||
|
|
335616c20f | ||
|
|
d213399161 | ||
|
|
1f371e2807 | ||
|
|
4de1bbd6f7 | ||
|
|
81d660447d | ||
|
|
a34f5dea69 | ||
|
|
6c62c1a0fc | ||
|
|
42be51bc8f | ||
|
|
88002e7e8c | ||
|
|
6d3a3933cd | ||
|
|
9da75e02bc | ||
|
|
4ce56f38b4 | ||
|
|
40b5b7e185 | ||
|
|
c55025d04e | ||
|
|
36ba6c5147 | ||
|
|
6a2f315b2f | ||
|
|
45fa827c04 | ||
|
|
4106d39c32 | ||
|
|
1979c86d0f | ||
|
|
12f764d584 | ||
|
|
85ebd12c30 | ||
|
|
5c17c24055 | ||
|
|
ce4910bc5d | ||
|
|
d8d0beef65 | ||
|
|
745e960f49 | ||
|
|
6d29fcf294 | ||
|
|
e7d41496dd | ||
|
|
e78ff05419 | ||
|
|
105a239d94 | ||
|
|
4de989c803 | ||
|
|
a7e9b486ba | ||
|
|
a77426593d | ||
|
|
526e22f88b | ||
|
|
4314804ca7 | ||
|
|
035348d8a0 | ||
|
|
70c68929d8 | ||
|
|
b17fd7c986 | ||
|
|
2f0f2dfa8a | ||
|
|
3e7cea7bd3 | ||
|
|
fcd29a547d | ||
|
|
3d0ba3e78c | ||
|
|
91613baa5c | ||
|
|
6eb8f6793e | ||
|
|
e5cc4a28cb | ||
|
|
eac9f07abf | ||
|
|
130e4c2efd | ||
|
|
93b5ca1985 | ||
|
|
aa057565b5 | ||
|
|
13d64a5875 | ||
|
|
03b63befa2 | ||
|
|
3c6c7597e0 | ||
|
|
71f36bdb0b | ||
|
|
d6fb648e23 | ||
|
|
343aec8f04 | ||
|
|
78f7f2ba85 | ||
|
|
a8e1932122 | ||
|
|
390f309088 | ||
|
|
5d54c3f840 | ||
|
|
4a7bad313b | ||
|
|
be4b0bb3c2 | ||
|
|
1b386f64f2 | ||
|
|
8dea6170a2 | ||
|
|
928b39a728 | ||
|
|
c23f24de2e | ||
|
|
ff4aaf7c32 | ||
|
|
2c28c555cf | ||
|
|
d00ed33678 | ||
|
|
6473a2b532 | ||
|
|
1d196286b7 | ||
|
|
71ae5a7301 |
26
.github/workflows/build_container.yaml
vendored
26
.github/workflows/build_container.yaml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
- "**.md"
|
||||
|
||||
env:
|
||||
GO_VERSION: "~1.20"
|
||||
GO_VERSION: "~1.22"
|
||||
IMAGE_NAME: "k8sgpt"
|
||||
defaults:
|
||||
run:
|
||||
@@ -22,7 +22,7 @@ defaults:
|
||||
jobs:
|
||||
prepare_ci_run:
|
||||
name: Prepare CI Run
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
GIT_SHA: ${{ steps.extract_branch.outputs.GIT_SHA }}
|
||||
BRANCH: ${{ steps.extract_branch.outputs.BRANCH }}
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract_branch
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
build_image:
|
||||
name: Build Container Image
|
||||
needs: prepare_ci_run
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BRANCH: ${{ needs.prepare_ci_run.outputs.BRANCH }}
|
||||
DATETIME: ${{ needs.prepare_ci_run.outputs.DATETIME }}
|
||||
@@ -70,14 +70,14 @@ jobs:
|
||||
RELEASE_REGISTRY: "localhost:5000/k8sgpt"
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
outputs: type=docker,dest=/tmp/${{ env.IMAGE_NAME }}-image.tar
|
||||
|
||||
- name: Upload image as artifact
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4
|
||||
with:
|
||||
name: ${{ env.IMAGE_NAME }}-image.tar
|
||||
path: /tmp/${{ env.IMAGE_NAME }}-image.tar
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
name: Upload images to ghcr registry
|
||||
needs: [ prepare_ci_run, build_image ]
|
||||
if: github.event_name == 'push' && needs.prepare_ci_run.outputs.NON_FORKED_AND_NON_ROBOT_RUN == 'true' # only run on push to main/maintenance branches
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATETIME: ${{ needs.prepare_ci_run.outputs.DATETIME }}
|
||||
BUILD_TIME: ${{ needs.prepare_ci_run.outputs.BUILD_TIME }}
|
||||
@@ -115,10 +115,10 @@ jobs:
|
||||
contents: read # Needed for checking out the repository
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
@@ -126,10 +126,10 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
|
||||
with:
|
||||
context: .
|
||||
file: ./container/Dockerfile
|
||||
|
||||
8
.github/workflows/golangci_lint.yaml
vendored
8
.github/workflows/golangci_lint.yaml
vendored
@@ -2,17 +2,19 @@ name: Run golangci-lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
golangci-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: golangci-lint
|
||||
uses: reviewdog/action-golangci-lint@24d4af2fc93f5b2b296229e8b0c0f658d25707af # v2
|
||||
uses: reviewdog/action-golangci-lint@7708105983c614f7a2725e2172908b7709d1c3e4 # v2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-pr-check
|
||||
golangci_lint_flags: "--timeout=240s"
|
||||
level: warning
|
||||
|
||||
44
.github/workflows/release.yaml
vendored
44
.github/workflows/release.yaml
vendored
@@ -23,9 +23,9 @@ jobs:
|
||||
# Release-please creates a PR that tracks all changes
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3.7.13
|
||||
- uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1
|
||||
id: release
|
||||
with:
|
||||
command: manifest
|
||||
@@ -40,18 +40,32 @@ jobs:
|
||||
- release-please
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: false
|
||||
dotnet: false
|
||||
haskell: false
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4
|
||||
uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5
|
||||
with:
|
||||
go-version: '1.20'
|
||||
go-version: '1.22'
|
||||
- name: Download Syft
|
||||
uses: anchore/sbom-action/download-syft@fd74a6fb98a204a1ad35bbfae0122c1a302ff88b # v0.15.0
|
||||
uses: anchore/sbom-action/download-syft@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5
|
||||
uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6
|
||||
with:
|
||||
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||
distribution: goreleaser
|
||||
@@ -59,12 +73,14 @@ jobs:
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.K8SGPT_BOT_SECRET }}
|
||||
- name: Update new version in krew-index
|
||||
uses: rajatjindal/krew-release-bot@3d9faef30a82761d610544f62afddca00993eef9 # v0.0.47
|
||||
|
||||
build-container:
|
||||
if: needs.release-please.outputs.releases_created == 'true'
|
||||
needs:
|
||||
- release-please
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
@@ -74,23 +90,23 @@ jobs:
|
||||
IMAGE_NAME: k8sgpt
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
|
||||
with:
|
||||
context: .
|
||||
file: ./container/Dockerfile
|
||||
@@ -104,14 +120,14 @@ jobs:
|
||||
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_TAG }}
|
||||
|
||||
- name: Generate SBOM
|
||||
uses: anchore/sbom-action@fd74a6fb98a204a1ad35bbfae0122c1a302ff88b # v0.15.0
|
||||
uses: anchore/sbom-action@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8
|
||||
with:
|
||||
image: ${{ env.IMAGE_TAG }}
|
||||
artifact-name: sbom-${{ env.IMAGE_NAME }}
|
||||
output-file: ./sbom-${{ env.IMAGE_NAME }}.spdx.json
|
||||
|
||||
- name: Attach SBOM to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2
|
||||
with:
|
||||
tag_name: ${{ needs.release-please.outputs.tag_name }}
|
||||
files: ./sbom-${{ env.IMAGE_NAME }}.spdx.json
|
||||
|
||||
4
.github/workflows/semantic_pr.yaml
vendored
4
.github/workflows/semantic_pr.yaml
vendored
@@ -10,13 +10,13 @@ defaults:
|
||||
shell: bash
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # Needed for checking out the repository
|
||||
pull-requests: read # Needed for reading prs
|
||||
steps:
|
||||
- name: Validate Pull Request
|
||||
uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f # v5.4.0
|
||||
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
19
.github/workflows/test.yaml
vendored
19
.github/workflows/test.yaml
vendored
@@ -9,23 +9,22 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
GO_VERSION: "~1.20"
|
||||
GO_VERSION: "~1.22"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4
|
||||
uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Unit Test
|
||||
run: make test
|
||||
|
||||
# - name: Fmt Test
|
||||
# run: fmtFiles=$(make fmt); if [ "$fmtFiles" != "" ];then exit 1; fi
|
||||
|
||||
- name: Run test
|
||||
run: go test ./... -coverprofile=coverage.txt
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
version: 2
|
||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||
# Make sure to check the documentation at https://goreleaser.com
|
||||
before:
|
||||
@@ -14,16 +15,18 @@ builds:
|
||||
- windows
|
||||
- darwin
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}}
|
||||
- -s -w -X main.commit={{.ShortCommit}}
|
||||
- -s -w -X main.Date={{.CommitDate}}
|
||||
- -s -w
|
||||
- -X main.version={{.Version}}
|
||||
- -X main.commit={{.ShortCommit}}
|
||||
- -X main.Date={{.CommitDate}}
|
||||
|
||||
nfpms:
|
||||
- file_name_template: '{{ .ProjectName }}_{{ .Arch }}'
|
||||
- file_name_template: "{{ .ProjectName }}_{{ .Arch }}"
|
||||
maintainer: "K8sGPT Maintainers <contact@k8sgpt.ai>"
|
||||
homepage: https://k8sgpt.ai
|
||||
description: >-
|
||||
K8sGPT is a tool for scanning your kubernetes clusters, diagnosing and triaging issues in simple english. It has SRE experience codified into it’s analyzers and helps to pull out the most relevant information to enrich it with AI.
|
||||
license: "MIT"
|
||||
license: "Apache-2.0"
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
@@ -32,7 +35,7 @@ nfpms:
|
||||
section: utils
|
||||
contents:
|
||||
- src: ./LICENSE
|
||||
dst: /usr/share/doc/nfpm/copyright
|
||||
dst: /usr/share/doc/k8sgpt/copyright
|
||||
file_info:
|
||||
mode: 0644
|
||||
|
||||
@@ -51,25 +54,23 @@ archives:
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
brews:
|
||||
- name: k8sgpt
|
||||
homepage: https://k8sgpt.ai
|
||||
tap:
|
||||
repository:
|
||||
owner: k8sgpt-ai
|
||||
name: homebrew-k8sgpt
|
||||
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
name_template: "checksums.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
changelog:
|
||||
skip: true
|
||||
|
||||
# skip: true
|
||||
# The lines beneath this are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
|
||||
110
.krew.yaml
Normal file
110
.krew.yaml
Normal file
@@ -0,0 +1,110 @@
|
||||
apiVersion: krew.googlecontainertools.github.com/v1alpha2
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: gpt
|
||||
spec:
|
||||
version: {{ .TagName }}
|
||||
homepage: https://github.com/k8sgpt-ai/k8sgpt
|
||||
shortDescription: "Giving Kubernetes Superpowers to everyone"
|
||||
description: |
|
||||
A tool for scanning your Kubernetes clusters, diagnosing, and triaging issues in simple English.
|
||||
platforms:
|
||||
##########
|
||||
# Darwin #
|
||||
##########
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: amd64
|
||||
{{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Darwin_x86_64.tar.gz" .TagName | indent 6 }}
|
||||
files:
|
||||
- from: "k8sgpt"
|
||||
to: "kubectl-gpt"
|
||||
- from: "LICENSE"
|
||||
to: "."
|
||||
bin: kubectl-gpt
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: arm64
|
||||
{{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Darwin_arm64.tar.gz" .TagName | indent 6 }}
|
||||
files:
|
||||
- from: "k8sgpt"
|
||||
to: "kubectl-gpt"
|
||||
- from: "LICENSE"
|
||||
to: "."
|
||||
bin: kubectl-gpt
|
||||
|
||||
#########
|
||||
# Linux #
|
||||
#########
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: amd64
|
||||
{{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Linux_x86_64.tar.gz" .TagName | indent 6 }}
|
||||
files:
|
||||
- from: "k8sgpt"
|
||||
to: "kubectl-gpt"
|
||||
- from: "LICENSE"
|
||||
to: "."
|
||||
bin: kubectl-gpt
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: arm64
|
||||
{{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Linux_arm64.tar.gz" .TagName | indent 6 }}
|
||||
files:
|
||||
- from: "k8sgpt"
|
||||
to: "kubectl-gpt"
|
||||
- from: "LICENSE"
|
||||
to: "."
|
||||
bin: kubectl-gpt
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: "386"
|
||||
{{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Linux_i386.tar.gz" .TagName | indent 6 }}
|
||||
files:
|
||||
- from: "k8sgpt"
|
||||
to: "kubectl-gpt"
|
||||
- from: "LICENSE"
|
||||
to: "."
|
||||
bin: kubectl-gpt
|
||||
|
||||
###########
|
||||
# Windows #
|
||||
###########
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: amd64
|
||||
{{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Windows_x86_64.zip" .TagName | indent 6 }}
|
||||
files:
|
||||
- from: "k8sgpt"
|
||||
to: "kubectl-gpt"
|
||||
- from: "LICENSE"
|
||||
to: "."
|
||||
bin: kubectl-gpt
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: arm64
|
||||
{{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Windows_arm64.zip" .TagName | indent 6 }}
|
||||
files:
|
||||
- from: "k8sgpt"
|
||||
to: "kubectl-gpt"
|
||||
- from: "LICENSE"
|
||||
to: "."
|
||||
bin: kubectl-gpt
|
||||
- selector:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: "386"
|
||||
{{addURIAndSha "https://github.com/k8sgpt-ai/k8sgpt/releases/download/{{ .TagName }}/k8sgpt_Windows_i386.zip" .TagName | indent 6 }}
|
||||
files:
|
||||
- from: "k8sgpt"
|
||||
to: "kubectl-gpt"
|
||||
- from: "LICENSE"
|
||||
to: "."
|
||||
bin: kubectl-gpt
|
||||
@@ -1 +1 @@
|
||||
{".":"0.3.23"}
|
||||
{".":"0.3.48"}
|
||||
611
CHANGELOG.md
611
CHANGELOG.md
@@ -1,5 +1,616 @@
|
||||
# Changelog
|
||||
|
||||
## [0.3.48](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.47...v0.3.48) (2024-12-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* fixed missing cache params ([#1340](https://github.com/k8sgpt-ai/k8sgpt/issues/1340)) ([1363219](https://github.com/k8sgpt-ai/k8sgpt/commit/1363219b1b94e157ef03c53eba8838b7cef559b4))
|
||||
|
||||
## [0.3.47](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.46...v0.3.47) (2024-12-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add new AWS Bedrock model ids ([#1330](https://github.com/k8sgpt-ai/k8sgpt/issues/1330)) ([a12aa07](https://github.com/k8sgpt-ai/k8sgpt/commit/a12aa07b1a2e34c5106b7b930b29b0c97b172dc4))
|
||||
* adds interplex as a caching provider ([#1328](https://github.com/k8sgpt-ai/k8sgpt/issues/1328)) ([d6d80ee](https://github.com/k8sgpt-ai/k8sgpt/commit/d6d80ee86083643d9b91457791bfc77ef475e82e))
|
||||
* dump ([#1322](https://github.com/k8sgpt-ai/k8sgpt/issues/1322)) ([da266b3](https://github.com/k8sgpt-ai/k8sgpt/commit/da266b3c82ca8b3e96461be688a9f30e408568fe))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add maxTokens to serve mode ([#1280](https://github.com/k8sgpt-ai/k8sgpt/issues/1280)) ([a50375c](https://github.com/k8sgpt-ai/k8sgpt/commit/a50375c9605a87546a0fcbcacabe5482fdfa1c2c))
|
||||
* **deps:** update all non-major dependencies ([#1323](https://github.com/k8sgpt-ai/k8sgpt/issues/1323)) ([b3f60b2](https://github.com/k8sgpt-ai/k8sgpt/commit/b3f60b2d2018d4bede3918adcb3547ef2acf6688))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1303](https://github.com/k8sgpt-ai/k8sgpt/issues/1303)) ([2da0573](https://github.com/k8sgpt-ai/k8sgpt/commit/2da057360b378d34126e1480ade0686f104e3ace))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1321](https://github.com/k8sgpt-ai/k8sgpt/issues/1321)) ([69c67bd](https://github.com/k8sgpt-ai/k8sgpt/commit/69c67bd1d9d4404816a8b7a00c98499729f2185f))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1326](https://github.com/k8sgpt-ai/k8sgpt/issues/1326)) ([5514ebb](https://github.com/k8sgpt-ai/k8sgpt/commit/5514ebb53b79b5bac0fc861ffdebc9399fe87b62))
|
||||
* update OpenAI API key generation URL to reflect new platform link ([#1331](https://github.com/k8sgpt-ai/k8sgpt/issues/1331)) ([ec5e42b](https://github.com/k8sgpt-ai/k8sgpt/commit/ec5e42b8f43e90632bb62dd89cc6aa3665e0f60d))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update all non-major dependencies ([#1327](https://github.com/k8sgpt-ai/k8sgpt/issues/1327)) ([a841568](https://github.com/k8sgpt-ai/k8sgpt/commit/a841568a9c4c0012291cf8f4248250192b72a383))
|
||||
* **deps:** update codecov/codecov-action action to v5 ([#1324](https://github.com/k8sgpt-ai/k8sgpt/issues/1324)) ([cb1e1ff](https://github.com/k8sgpt-ai/k8sgpt/commit/cb1e1ffede1d3086d54157142c6803341e560ca8))
|
||||
* **deps:** update codecov/codecov-action digest to 015f24e ([#1325](https://github.com/k8sgpt-ai/k8sgpt/issues/1325)) ([4d7eb0f](https://github.com/k8sgpt-ai/k8sgpt/commit/4d7eb0f6226fc50f58b5c2fff7534dd16e2ca378))
|
||||
* **deps:** update docker/build-push-action action to v6 ([#1294](https://github.com/k8sgpt-ai/k8sgpt/issues/1294)) ([f37d923](https://github.com/k8sgpt-ai/k8sgpt/commit/f37d92391877819c6d26a993ab58bc0c49fb3b66))
|
||||
* **deps:** update docker/build-push-action digest to 48aba3b ([#1333](https://github.com/k8sgpt-ai/k8sgpt/issues/1333)) ([c21ba86](https://github.com/k8sgpt-ai/k8sgpt/commit/c21ba86237db651086c0a37abc3454db513e505b))
|
||||
* **deps:** update rajatjindal/krew-release-bot action to v0.0.47 ([#1317](https://github.com/k8sgpt-ai/k8sgpt/issues/1317)) ([896a53b](https://github.com/k8sgpt-ai/k8sgpt/commit/896a53be8394c490e2d34f151de44c3663dddf5b))
|
||||
|
||||
## [0.3.46](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.45...v0.3.46) (2024-11-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* reverting the cncf runners ([#1319](https://github.com/k8sgpt-ai/k8sgpt/issues/1319)) ([ad86e7a](https://github.com/k8sgpt-ai/k8sgpt/commit/ad86e7aa39995c492437627dbd9f89f152f11f2c))
|
||||
* switching to higher spec runners ([#1312](https://github.com/k8sgpt-ai/k8sgpt/issues/1312)) ([5f7d9de](https://github.com/k8sgpt-ai/k8sgpt/commit/5f7d9de46a521463cedc901b729fe27f8d86f381))
|
||||
* testupdate ([#1315](https://github.com/k8sgpt-ai/k8sgpt/issues/1315)) ([7dcdfc8](https://github.com/k8sgpt-ai/k8sgpt/commit/7dcdfc83d2461e4342ded5fa80493936b70f64a1))
|
||||
* updated runners to enterprise ([#1318](https://github.com/k8sgpt-ai/k8sgpt/issues/1318)) ([1ae70e8](https://github.com/k8sgpt-ai/k8sgpt/commit/1ae70e806e2609c8fb964f0a577304d07b365cae))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/setup-go digest to 41dfa10 ([#1284](https://github.com/k8sgpt-ai/k8sgpt/issues/1284)) ([2ce8450](https://github.com/k8sgpt-ai/k8sgpt/commit/2ce8450e03986904a7ffe7afac4b5ba777c67c57))
|
||||
* **deps:** update softprops/action-gh-release action to v2 ([#1295](https://github.com/k8sgpt-ai/k8sgpt/issues/1295)) ([b6b3d0c](https://github.com/k8sgpt-ai/k8sgpt/commit/b6b3d0c8566b0dbd9cb0e5f59c8493e4343e0106))
|
||||
|
||||
## [0.3.45](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.44...v0.3.45) (2024-11-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* free disk ([#1313](https://github.com/k8sgpt-ai/k8sgpt/issues/1313)) ([783cd1c](https://github.com/k8sgpt-ai/k8sgpt/commit/783cd1cfc66f8e4489e5006529745d8caf38cfd4))
|
||||
|
||||
## [0.3.44](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.43...v0.3.44) (2024-11-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* test revert runner on release job ([#1310](https://github.com/k8sgpt-ai/k8sgpt/issues/1310)) ([cc9b3ea](https://github.com/k8sgpt-ai/k8sgpt/commit/cc9b3ea6579c6190629e0fac48e37e0eba650158))
|
||||
|
||||
## [0.3.43](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.42...v0.3.43) (2024-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1296](https://github.com/k8sgpt-ai/k8sgpt/issues/1296)) ([2f75986](https://github.com/k8sgpt-ai/k8sgpt/commit/2f759865b6fc5ae143c8f5e89a306abc89d4de27))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update dependency ubuntu to v24 ([#1293](https://github.com/k8sgpt-ai/k8sgpt/issues/1293)) ([c67add3](https://github.com/k8sgpt-ai/k8sgpt/commit/c67add30c64257ac6258dec93193e3201ba8c4ab))
|
||||
|
||||
## [0.3.42](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.41...v0.3.42) (2024-11-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add stats option to analyze command for performance insights ([#1237](https://github.com/k8sgpt-ai/k8sgpt/issues/1237)) ([3eec9bb](https://github.com/k8sgpt-ai/k8sgpt/commit/3eec9bbb05b2f0717437cc4a2ec786594ece1cc3))
|
||||
* error from events for STS analyzer ([#1256](https://github.com/k8sgpt-ai/k8sgpt/issues/1256)) ([d8fad95](https://github.com/k8sgpt-ai/k8sgpt/commit/d8fad956f45a4dd668647379bb0295e169faeac6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [Bug] Make lint command is not working ([#1282](https://github.com/k8sgpt-ai/k8sgpt/issues/1282)) ([87565a0](https://github.com/k8sgpt-ai/k8sgpt/commit/87565a0bcce7087114798c3a32877894c8a9dcee))
|
||||
* add providerId to serve mode ([#1260](https://github.com/k8sgpt-ai/k8sgpt/issues/1260)) ([da0764d](https://github.com/k8sgpt-ai/k8sgpt/commit/da0764d951ca76cb7007c412f8efa794619c20ba))
|
||||
* **deps:** update all non-major dependencies ([#1291](https://github.com/k8sgpt-ai/k8sgpt/issues/1291)) ([14e0f19](https://github.com/k8sgpt-ai/k8sgpt/commit/14e0f19b12189052b03d551e409b407fd0b6bd30))
|
||||
* **deps:** update k8s.io/utils digest to 49e7df5 ([#1259](https://github.com/k8sgpt-ai/k8sgpt/issues/1259)) ([7785dd1](https://github.com/k8sgpt-ai/k8sgpt/commit/7785dd12a0245a33af25dedd2fbb5f4178b5cda9))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go to v1.35.1-20240920204244-7a91c8620515.1 ([#1274](https://github.com/k8sgpt-ai/k8sgpt/issues/1274)) ([9f39abf](https://github.com/k8sgpt-ai/k8sgpt/commit/9f39abf89e4e92009f5e138d9b01d11c60ac135c))
|
||||
* **deps:** update module cloud.google.com/go/storage to v1.44.0 ([#1265](https://github.com/k8sgpt-ai/k8sgpt/issues/1265)) ([4143e9f](https://github.com/k8sgpt-ai/k8sgpt/commit/4143e9fd524bed3179524d949b7b0f92c02ecd11))
|
||||
* **deps:** update module github.com/adrg/xdg to v0.5.0 ([#1262](https://github.com/k8sgpt-ai/k8sgpt/issues/1262)) ([98237b6](https://github.com/k8sgpt-ai/k8sgpt/commit/98237b6408521ee7afc05fcaed2f78ba79e77144))
|
||||
* **deps:** update module github.com/aquasecurity/trivy-operator to v0.22.0 ([#1034](https://github.com/k8sgpt-ai/k8sgpt/issues/1034)) ([037e745](https://github.com/k8sgpt-ai/k8sgpt/commit/037e745c6f667830f0e1d531ce4bbd07083ef972))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.55.5 ([#1263](https://github.com/k8sgpt-ai/k8sgpt/issues/1263)) ([0148a5b](https://github.com/k8sgpt-ai/k8sgpt/commit/0148a5b3549cbdb6c6e5832dc01aab044b90ddc9))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.8.0 ([#1264](https://github.com/k8sgpt-ai/k8sgpt/issues/1264)) ([3613585](https://github.com/k8sgpt-ai/k8sgpt/commit/36135857ac55e126b3a6c4533a000cb0b7f32c6b))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.4.1 ([#1275](https://github.com/k8sgpt-ai/k8sgpt/issues/1275)) ([c9b11b6](https://github.com/k8sgpt-ai/k8sgpt/commit/c9b11b6eee00d0269a4d48ad2e4be5458436b51d))
|
||||
* **deps:** update module github.com/cohere-ai/cohere-go/v2 to v2.12.0 ([#1276](https://github.com/k8sgpt-ai/k8sgpt/issues/1276)) ([7a3fb3c](https://github.com/k8sgpt-ai/k8sgpt/commit/7a3fb3cf6777d5b0babf00455c3833a47bb1bfdb))
|
||||
* **deps:** update module github.com/google/generative-ai-go to v0.18.0 ([#1278](https://github.com/k8sgpt-ai/k8sgpt/issues/1278)) ([ad349ae](https://github.com/k8sgpt-ai/k8sgpt/commit/ad349ae263f226e300f60dd092729c5a3bf61dbe))
|
||||
* rename watsonxai to ibmwatsonxai ([#1234](https://github.com/k8sgpt-ai/k8sgpt/issues/1234)) ([5ff6dc9](https://github.com/k8sgpt-ai/k8sgpt/commit/5ff6dc9be5218e47839c4ac5e8f3458b40eb9c88))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/checkout digest to 11bd719 ([#1283](https://github.com/k8sgpt-ai/k8sgpt/issues/1283)) ([0cfecbd](https://github.com/k8sgpt-ai/k8sgpt/commit/0cfecbdd87586fd138cc63c4e7a26d54e7ed83a8))
|
||||
* **deps:** update actions/checkout digest to eef6144 ([#1270](https://github.com/k8sgpt-ai/k8sgpt/issues/1270)) ([72eb815](https://github.com/k8sgpt-ai/k8sgpt/commit/72eb8159fb4a2284cf43eb6a5f3de7bed10c6224))
|
||||
* **deps:** update actions/upload-artifact digest to b4b15b8 ([#1272](https://github.com/k8sgpt-ai/k8sgpt/issues/1272)) ([911d578](https://github.com/k8sgpt-ai/k8sgpt/commit/911d578bf006253d10fe21d96888ddf34a8b4691))
|
||||
* **deps:** update anchore/sbom-action action to v0.17.2 ([#1248](https://github.com/k8sgpt-ai/k8sgpt/issues/1248)) ([04582d8](https://github.com/k8sgpt-ai/k8sgpt/commit/04582d85160055da30e4e00fd3c6ca69d1decd1a))
|
||||
* **deps:** update anchore/sbom-action action to v0.17.4 ([#1273](https://github.com/k8sgpt-ai/k8sgpt/issues/1273)) ([c128bf7](https://github.com/k8sgpt-ai/k8sgpt/commit/c128bf7942e380fcab5e9771f405471198e388fe))
|
||||
* **deps:** update anchore/sbom-action action to v0.17.6 ([#1285](https://github.com/k8sgpt-ai/k8sgpt/issues/1285)) ([173e4dc](https://github.com/k8sgpt-ai/k8sgpt/commit/173e4dc5ac6265af4a3538556220d3a43ab721f7))
|
||||
* **deps:** update codecov/codecov-action action to v4 ([#1292](https://github.com/k8sgpt-ai/k8sgpt/issues/1292)) ([c1a38c2](https://github.com/k8sgpt-ai/k8sgpt/commit/c1a38c2b35a0bfa772b88f15843c9354b0345284))
|
||||
* **deps:** update docker/setup-buildx-action digest to c47758b ([#1213](https://github.com/k8sgpt-ai/k8sgpt/issues/1213)) ([161bc11](https://github.com/k8sgpt-ai/k8sgpt/commit/161bc11294d5094533068cf7af9880795a61536e))
|
||||
* **deps:** update golang docker tag to v1.23 ([#1254](https://github.com/k8sgpt-ai/k8sgpt/issues/1254)) ([b62b7db](https://github.com/k8sgpt-ai/k8sgpt/commit/b62b7dbe3c9cd02b81f6a0111bca939034c5cc9f))
|
||||
* **deps:** update module github.com/docker/docker to v27.3.1+incompatible ([#1225](https://github.com/k8sgpt-ai/k8sgpt/issues/1225)) ([9c1927b](https://github.com/k8sgpt-ai/k8sgpt/commit/9c1927b4975fa8132fbc24dd96a5737819855544))
|
||||
* renovate.json ([#1290](https://github.com/k8sgpt-ai/k8sgpt/issues/1290)) ([458fcfe](https://github.com/k8sgpt-ai/k8sgpt/commit/458fcfe8d330523781d32af680febc2a0c0525a2))
|
||||
|
||||
## [0.3.41](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.40...v0.3.41) (2024-09-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add custom-analyzer cmd ([#1207](https://github.com/k8sgpt-ai/k8sgpt/issues/1207)) ([db26d24](https://github.com/k8sgpt-ai/k8sgpt/commit/db26d24ac607534ce78c1c82f3e1d4e5dde17578))
|
||||
* add event failure handling in service analyzer ([#1132](https://github.com/k8sgpt-ai/k8sgpt/issues/1132)) ([a4e44d5](https://github.com/k8sgpt-ai/k8sgpt/commit/a4e44d59e3ee63714cfd144228299e4f24ac3691))
|
||||
* added support for A21 and Amazon Titan models via bedrock api ([#1101](https://github.com/k8sgpt-ai/k8sgpt/issues/1101)) ([4f3ecf0](https://github.com/k8sgpt-ai/k8sgpt/commit/4f3ecf008351075068738e930ff3a657f597654a))
|
||||
* adding a query mode for the schednex scheduler ([#1257](https://github.com/k8sgpt-ai/k8sgpt/issues/1257)) ([53465d5](https://github.com/k8sgpt-ai/k8sgpt/commit/53465d5c832ac490403a2698b80122ca06372df7))
|
||||
* refactoring to the new schema ([#1219](https://github.com/k8sgpt-ai/k8sgpt/issues/1219)) ([02fa109](https://github.com/k8sgpt-ai/k8sgpt/commit/02fa109429d3c684079f5d488e7f517806fc1a09))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to 702e33f ([#1246](https://github.com/k8sgpt-ai/k8sgpt/issues/1246)) ([d30563d](https://github.com/k8sgpt-ai/k8sgpt/commit/d30563d8cdedb5bbf48735e49ebcb44440a5f0f5))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 to v2.22.0-20240807134501-ea98c104104d.1 ([#1186](https://github.com/k8sgpt-ai/k8sgpt/issues/1186)) ([8405778](https://github.com/k8sgpt-ai/k8sgpt/commit/8405778cb25429d2b42d7a3b50ec88b45961a57f))
|
||||
* **deps:** update module github.com/docker/docker to v27.1.1+incompatible [security] ([#1220](https://github.com/k8sgpt-ai/k8sgpt/issues/1220)) ([3148b5c](https://github.com/k8sgpt-ai/k8sgpt/commit/3148b5c61d2ff57d67d966d6e915994d4aa8a844))
|
||||
* **deps:** update module github.com/mittwald/go-helm-client to v0.12.12 ([#1226](https://github.com/k8sgpt-ai/k8sgpt/issues/1226)) ([7019d0b](https://github.com/k8sgpt-ai/k8sgpt/commit/7019d0b62f1bebbd4c2a251c98a2beb4975bf2fe))
|
||||
* **deps:** update module github.com/mittwald/go-helm-client to v0.12.13 ([#1251](https://github.com/k8sgpt-ai/k8sgpt/issues/1251)) ([1dfd139](https://github.com/k8sgpt-ai/k8sgpt/commit/1dfd13973165bd2820aa8ca079e1ec656a5033f0))
|
||||
* **deps:** update module github.com/schollz/progressbar/v3 to v3.15.0 ([#1227](https://github.com/k8sgpt-ai/k8sgpt/issues/1227)) ([025a069](https://github.com/k8sgpt-ai/k8sgpt/commit/025a069ff1582131cede63420aa535a3b550b7b7))
|
||||
* disable adding multiple openai provider ([#1191](https://github.com/k8sgpt-ai/k8sgpt/issues/1191)) ([644581f](https://github.com/k8sgpt-ai/k8sgpt/commit/644581f4958f470cfb088a69a478db0ab91c1540))
|
||||
* enabled auth add support watsonx backend ([#1190](https://github.com/k8sgpt-ai/k8sgpt/issues/1190)) ([d702209](https://github.com/k8sgpt-ai/k8sgpt/commit/d702209941480dce62b9622ea30fdb4a9e5ef083))
|
||||
* helm chart security context rendering if empty ([#1235](https://github.com/k8sgpt-ai/k8sgpt/issues/1235)) ([be4ca86](https://github.com/k8sgpt-ai/k8sgpt/commit/be4ca86af07e832eb7832f7e5f83df8676bafd29))
|
||||
* issue-1168, remove duplicate CVE ([#1230](https://github.com/k8sgpt-ai/k8sgpt/issues/1230)) ([8edb053](https://github.com/k8sgpt-ai/k8sgpt/commit/8edb053b3e88027880a75999eab19bed2176747f))
|
||||
* segmentation violation during serve ([#1215](https://github.com/k8sgpt-ai/k8sgpt/issues/1215)) ([b7e5394](https://github.com/k8sgpt-ai/k8sgpt/commit/b7e5394caaabb43e01161618f7a6e9f4aa8f7408))
|
||||
* set logger for controller-runtime ([#1211](https://github.com/k8sgpt-ai/k8sgpt/issues/1211)) ([8e37369](https://github.com/k8sgpt-ai/k8sgpt/commit/8e37369e5c6c96096b66179f22a27b2c0018c43a))
|
||||
* typo ([#1244](https://github.com/k8sgpt-ai/k8sgpt/issues/1244)) ([e02c0dd](https://github.com/k8sgpt-ai/k8sgpt/commit/e02c0ddd2d9f9a6fae8a57514468f26fe72b567a))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/checkout digest to 692973e ([#1129](https://github.com/k8sgpt-ai/k8sgpt/issues/1129)) ([24ebeaf](https://github.com/k8sgpt-ai/k8sgpt/commit/24ebeaf3a748f2bf40c18ddcecaf8655b457048b))
|
||||
* **deps:** update actions/upload-artifact digest to 5076954 ([#1239](https://github.com/k8sgpt-ai/k8sgpt/issues/1239)) ([e0e86ea](https://github.com/k8sgpt-ai/k8sgpt/commit/e0e86ea60f3811e8ee22fd9c28e91817c56104a2))
|
||||
* **deps:** update actions/upload-artifact digest to 834a144 ([#1214](https://github.com/k8sgpt-ai/k8sgpt/issues/1214)) ([2a8a9b4](https://github.com/k8sgpt-ai/k8sgpt/commit/2a8a9b486714d780c0df3ecae8757534249731dc))
|
||||
* **deps:** update anchore/sbom-action action to v0.17.1 ([#1224](https://github.com/k8sgpt-ai/k8sgpt/issues/1224)) ([f573819](https://github.com/k8sgpt-ai/k8sgpt/commit/f57381961fbc63305d9e9aa63e85a90a100ee553))
|
||||
* **deps:** update dependency go to v1.23.1 ([#1176](https://github.com/k8sgpt-ai/k8sgpt/issues/1176)) ([453d5c3](https://github.com/k8sgpt-ai/k8sgpt/commit/453d5c37ddafd93c6fa194b5b4fc0794154eb8c1))
|
||||
* **deps:** update docker/login-action digest to 9780b0c ([#1212](https://github.com/k8sgpt-ai/k8sgpt/issues/1212)) ([477ef15](https://github.com/k8sgpt-ai/k8sgpt/commit/477ef155d32f4d81ca3bee612644f51fc1098cdc))
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
* update "CLI Installation" section in README.md ([#1126](https://github.com/k8sgpt-ai/k8sgpt/issues/1126)) ([#1127](https://github.com/k8sgpt-ai/k8sgpt/issues/1127)) ([b2b8682](https://github.com/k8sgpt-ai/k8sgpt/commit/b2b86826e55984c2b6aed6554869d7ce66a5f854))
|
||||
|
||||
## [0.3.40](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.39...v0.3.40) (2024-08-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* custom analysis paralelism ([#1203](https://github.com/k8sgpt-ai/k8sgpt/issues/1203)) ([f1b7b37](https://github.com/k8sgpt-ai/k8sgpt/commit/f1b7b37fb83937d5fad90d7b6b52f4a38823da9e))
|
||||
* getting the error from status field for HPA analyzer ([#1164](https://github.com/k8sgpt-ai/k8sgpt/issues/1164)) ([a068310](https://github.com/k8sgpt-ai/k8sgpt/commit/a068310731d775beecede03a1709e541ffd68142))
|
||||
* initial custom analysis server mode ([#1205](https://github.com/k8sgpt-ai/k8sgpt/issues/1205)) ([16d57e5](https://github.com/k8sgpt-ai/k8sgpt/commit/16d57e5a55c2084bf1580377ae52e2961cc84922))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add default maxToken value of watsonxai backend ([#1209](https://github.com/k8sgpt-ai/k8sgpt/issues/1209)) ([d43fd87](https://github.com/k8sgpt-ai/k8sgpt/commit/d43fd878ba04fec8ac8afe4a1c15272b7f21c951))
|
||||
* auth update throw out exception ([#1193](https://github.com/k8sgpt-ai/k8sgpt/issues/1193)) ([391a3cd](https://github.com/k8sgpt-ai/k8sgpt/commit/391a3cd5adcbd90f37922332b4fad5ba5d813e5f))
|
||||
* **deps:** update module cloud.google.com/go/storage to v1.43.0 ([#1198](https://github.com/k8sgpt-ai/k8sgpt/issues/1198)) ([8949f5b](https://github.com/k8sgpt-ai/k8sgpt/commit/8949f5bac3c69130e30103511fdb5ece66e1619f))
|
||||
* **deps:** update module github.com/schollz/progressbar/v3 to v3.14.5 ([#1145](https://github.com/k8sgpt-ai/k8sgpt/issues/1145)) ([3547c48](https://github.com/k8sgpt-ai/k8sgpt/commit/3547c4808a846eb4392996afa20a84bdddf8e24f))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update anchore/sbom-action action to v0.17.0 ([#1197](https://github.com/k8sgpt-ai/k8sgpt/issues/1197)) ([407c855](https://github.com/k8sgpt-ai/k8sgpt/commit/407c855e147b73739e800310c926826344d36323))
|
||||
|
||||
## [0.3.39](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.38...v0.3.39) (2024-07-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add label selector ([#1201](https://github.com/k8sgpt-ai/k8sgpt/issues/1201)) ([eb3b81f](https://github.com/k8sgpt-ai/k8sgpt/commit/eb3b81f1767c589474864992ae78001ab1b376a1))
|
||||
* fix the custom-analysis printing ([#1195](https://github.com/k8sgpt-ai/k8sgpt/issues/1195)) ([b6dd2a1](https://github.com/k8sgpt-ai/k8sgpt/commit/b6dd2a1181b478a4fb8543ab7529ce595fa7d4a8))
|
||||
* initial kyverno support ([#1200](https://github.com/k8sgpt-ai/k8sgpt/issues/1200)) ([5176759](https://github.com/k8sgpt-ai/k8sgpt/commit/5176759bd0fad8671164f9e75b31dec19f02bd54))
|
||||
* skip k3s node type EtcdIsVoter ([#1167](https://github.com/k8sgpt-ai/k8sgpt/issues/1167)) ([4366ad9](https://github.com/k8sgpt-ai/k8sgpt/commit/4366ad97b80d2df0400e06e4b892fadab3939dc7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to 18e509b ([#1183](https://github.com/k8sgpt-ai/k8sgpt/issues/1183)) ([0b90651](https://github.com/k8sgpt-ai/k8sgpt/commit/0b906511d5a9837c9a67cf819754c610b1becc5c))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.4.0-20240715142657-3785f0a44aae.2 ([#1196](https://github.com/k8sgpt-ai/k8sgpt/issues/1196)) ([f9edbf3](https://github.com/k8sgpt-ai/k8sgpt/commit/f9edbf34f3eb3e90528d04b1c470fd6ef15293ec))
|
||||
* **deps:** update module github.com/ibm/watsonx-go to v1.0.1 ([#1187](https://github.com/k8sgpt-ai/k8sgpt/issues/1187)) ([34b6de3](https://github.com/k8sgpt-ai/k8sgpt/commit/34b6de34041ce253c1c680a7f5fe535b03a50da5))
|
||||
* **deps:** update module github.com/prometheus/prometheus to v0.53.1 ([#1035](https://github.com/k8sgpt-ai/k8sgpt/issues/1035)) ([de9ef85](https://github.com/k8sgpt-ai/k8sgpt/commit/de9ef8587822814542661e0039b47ef65d902abb))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** pin goreleaser/goreleaser-action action to 286f3b1 ([#1171](https://github.com/k8sgpt-ai/k8sgpt/issues/1171)) ([1a00aaf](https://github.com/k8sgpt-ai/k8sgpt/commit/1a00aafbb2f6f1482dfb3da7e96954b12ad5a4fd))
|
||||
* **deps:** update actions/setup-go digest to 0a12ed9 ([#1182](https://github.com/k8sgpt-ai/k8sgpt/issues/1182)) ([593139c](https://github.com/k8sgpt-ai/k8sgpt/commit/593139cffb1982fe45ccc9403acc893f51064271))
|
||||
* **deps:** update actions/upload-artifact digest to 0b2256b ([#1175](https://github.com/k8sgpt-ai/k8sgpt/issues/1175)) ([4b13727](https://github.com/k8sgpt-ai/k8sgpt/commit/4b13727ef579240adc2777d1126544fafb23b993))
|
||||
* **deps:** update anchore/sbom-action action to v0.16.1 ([#1179](https://github.com/k8sgpt-ai/k8sgpt/issues/1179)) ([3e93409](https://github.com/k8sgpt-ai/k8sgpt/commit/3e9340925c3d59861b1a95d5c1bc08c19ec26e4a))
|
||||
|
||||
## [0.3.38](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.37...v0.3.38) (2024-07-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add custom http headers to openai related api backends ([#1174](https://github.com/k8sgpt-ai/k8sgpt/issues/1174)) ([02e754e](https://github.com/k8sgpt-ai/k8sgpt/commit/02e754ed591742fccc5ff9a20c3e36e4475f6ec5))
|
||||
* add Ollama backend ([#1065](https://github.com/k8sgpt-ai/k8sgpt/issues/1065)) ([b35dbd9](https://github.com/k8sgpt-ai/k8sgpt/commit/b35dbd9b09197994f041cda04f1a4e5fb316e468))
|
||||
* add watsonx ai provider ([#1163](https://github.com/k8sgpt-ai/k8sgpt/issues/1163)) ([ce63821](https://github.com/k8sgpt-ai/k8sgpt/commit/ce63821bebbd87b2e058f5cf58a2cdd474b8fb58))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 to v2.20.0-20240406062209-1cc152efbf5c.1 ([#1147](https://github.com/k8sgpt-ai/k8sgpt/issues/1147)) ([314f25a](https://github.com/k8sgpt-ai/k8sgpt/commit/314f25ac8bf5c3629474ece0eae6a3bda83099aa))
|
||||
* **deps:** update module github.com/mittwald/go-helm-client to v0.12.10 ([#1177](https://github.com/k8sgpt-ai/k8sgpt/issues/1177)) ([fef8539](https://github.com/k8sgpt-ai/k8sgpt/commit/fef853966fc6e33dae0a9686fa767b36201c0228))
|
||||
* **deps:** update module github.com/spf13/cobra to v1.8.1 ([#1161](https://github.com/k8sgpt-ai/k8sgpt/issues/1161)) ([a075792](https://github.com/k8sgpt-ai/k8sgpt/commit/a0757921191205398539a6ccc8dbfaa503db595f))
|
||||
* **deps:** update module google.golang.org/grpc to v1.64.1 [security] ([#1178](https://github.com/k8sgpt-ai/k8sgpt/issues/1178)) ([dd20dbc](https://github.com/k8sgpt-ai/k8sgpt/commit/dd20dbc9829fc50f77ad6a32c3a10dcf221d2750))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update amannn/action-semantic-pull-request action to v5.5.3 ([#1172](https://github.com/k8sgpt-ai/k8sgpt/issues/1172)) ([27ac60a](https://github.com/k8sgpt-ai/k8sgpt/commit/27ac60aed296c3d9582f34e14c5985a4bccd991e))
|
||||
* **deps:** update anchore/sbom-action action to v0.16.0 ([#1146](https://github.com/k8sgpt-ai/k8sgpt/issues/1146)) ([dd66355](https://github.com/k8sgpt-ai/k8sgpt/commit/dd6635579789ce65ee86dc1196e7dfde1b7d20e6))
|
||||
* **deps:** update docker/build-push-action digest to ca052bb ([#1140](https://github.com/k8sgpt-ai/k8sgpt/issues/1140)) ([0c02160](https://github.com/k8sgpt-ai/k8sgpt/commit/0c0216096efde9c2c812ee90522c081f51c52631))
|
||||
* **deps:** update docker/setup-buildx-action digest to 4fd8129 ([#1173](https://github.com/k8sgpt-ai/k8sgpt/issues/1173)) ([d4abb33](https://github.com/k8sgpt-ai/k8sgpt/commit/d4abb33b3c29d9a2e4dee094ea7be2bc5d1807d1))
|
||||
* update brew installation note ([#1155](https://github.com/k8sgpt-ai/k8sgpt/issues/1155)) ([ab534d1](https://github.com/k8sgpt-ai/k8sgpt/commit/ab534d184fcd538f2ba10a6b5bf3a74c28d5fee6))
|
||||
|
||||
## [0.3.37](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.36...v0.3.37) (2024-06-17)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to 7708105 ([#1157](https://github.com/k8sgpt-ai/k8sgpt/issues/1157)) ([7b1b633](https://github.com/k8sgpt-ai/k8sgpt/commit/7b1b63322ec7b0c0864682bc23be6e70c0ed7ec7))
|
||||
* updated the goreleaser action ([#1160](https://github.com/k8sgpt-ai/k8sgpt/issues/1160)) ([9bace02](https://github.com/k8sgpt-ai/k8sgpt/commit/9bace02a6702a8af0e6511b51ffc38378e14d3cb))
|
||||
|
||||
## [0.3.36](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.35...v0.3.36) (2024-06-17)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update docker/login-action digest to 0d4c9c5 ([#1141](https://github.com/k8sgpt-ai/k8sgpt/issues/1141)) ([602d111](https://github.com/k8sgpt-ai/k8sgpt/commit/602d111d8568d38cda744d2b179ee2d3eb59ba02))
|
||||
* **deps:** update goreleaser/goreleaser-action digest to 5742e2a ([#1153](https://github.com/k8sgpt-ai/k8sgpt/issues/1153)) ([55ae7c3](https://github.com/k8sgpt-ai/k8sgpt/commit/55ae7c32986100d4b0bab6dcaf7a52ac7b37aa5f))
|
||||
* fixed the goreleaser file ([#1158](https://github.com/k8sgpt-ai/k8sgpt/issues/1158)) ([2382de4](https://github.com/k8sgpt-ai/k8sgpt/commit/2382de4c6f82de535b67c2752d7c502d0a8b2b66))
|
||||
* update goreleaser ldflags ([#1154](https://github.com/k8sgpt-ai/k8sgpt/issues/1154)) ([aeae2ba](https://github.com/k8sgpt-ai/k8sgpt/commit/aeae2ba765c7db6e4953b5a93c54617f1dd85efa))
|
||||
|
||||
## [0.3.35](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.34...v0.3.35) (2024-06-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add spec.template.spec.securityContext ([#1109](https://github.com/k8sgpt-ai/k8sgpt/issues/1109)) ([92dd1bd](https://github.com/k8sgpt-ai/k8sgpt/commit/92dd1bd8b08c5173f72a6c333f626c63aa05a1d3))
|
||||
* support openai organization Id ([#1133](https://github.com/k8sgpt-ai/k8sgpt/issues/1133)) ([4867d39](https://github.com/k8sgpt-ai/k8sgpt/commit/4867d39c66a6c16906cd769a2055dea9f66f1ccb))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* updated goreleaser config ([#1149](https://github.com/k8sgpt-ai/k8sgpt/issues/1149)) ([c834c09](https://github.com/k8sgpt-ai/k8sgpt/commit/c834c099969f3e888f49f73fba6794387063a6fc))
|
||||
|
||||
## [0.3.34](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.33...v0.3.34) (2024-06-14)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update google-github-actions/release-please-action action to v4.1.1 ([#1143](https://github.com/k8sgpt-ai/k8sgpt/issues/1143)) ([63b63f7](https://github.com/k8sgpt-ai/k8sgpt/commit/63b63f7664277042188351073f269569bfec65bf))
|
||||
* **deps:** update goreleaser/goreleaser-action digest to 5742e2a ([#1142](https://github.com/k8sgpt-ai/k8sgpt/issues/1142)) ([c101e8a](https://github.com/k8sgpt-ai/k8sgpt/commit/c101e8a3ea6d911d00ca2a51986edc5425a1042a))
|
||||
|
||||
## [0.3.33](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.32...v0.3.33) (2024-06-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* bump golang version to 1.22 ([#1117](https://github.com/k8sgpt-ai/k8sgpt/issues/1117)) ([6652fbe](https://github.com/k8sgpt-ai/k8sgpt/commit/6652fbe7cb6e581497e1d086e13397ff9e5b11be))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* advisory k8sgpt ghsa 85rg 8m6h 825p ([#1139](https://github.com/k8sgpt-ai/k8sgpt/issues/1139)) ([728555c](https://github.com/k8sgpt-ai/k8sgpt/commit/728555c0effbf7a56221d625bcbbf62f74d14359))
|
||||
* **deps:** typo in prometheus.go ([fad00ea](https://github.com/k8sgpt-ai/k8sgpt/commit/fad00eac4925351c4dc6fd6dd347fe2968f0b7a5))
|
||||
* **deps:** typo in prometheus.go ([#1137](https://github.com/k8sgpt-ai/k8sgpt/issues/1137)) ([fad00ea](https://github.com/k8sgpt-ai/k8sgpt/commit/fad00eac4925351c4dc6fd6dd347fe2968f0b7a5))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.53.21 ([#1106](https://github.com/k8sgpt-ai/k8sgpt/issues/1106)) ([bdd470f](https://github.com/k8sgpt-ai/k8sgpt/commit/bdd470f9cae917f965badd22da7def4a7d64d2ae))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.6.0 [security] ([#1138](https://github.com/k8sgpt-ai/k8sgpt/issues/1138)) ([3a89318](https://github.com/k8sgpt-ai/k8sgpt/commit/3a893184af50f8c822ac06ce0e20818eaec587b1))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/setup-go digest to cdcb360 ([#1096](https://github.com/k8sgpt-ai/k8sgpt/issues/1096)) ([3452c0d](https://github.com/k8sgpt-ai/k8sgpt/commit/3452c0def68fd5352d2d09201f813f657245bd9f))
|
||||
|
||||
## [0.3.32](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.31...v0.3.32) (2024-05-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove shorthand flag for topp option in add command ([#1115](https://github.com/k8sgpt-ai/k8sgpt/issues/1115)) ([e261c09](https://github.com/k8sgpt-ai/k8sgpt/commit/e261c09889359d5870acb9720ff033440f835f8f))
|
||||
|
||||
## [0.3.31](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.30...v0.3.31) (2024-05-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* implement Top-K sampling for improved user control ([#1110](https://github.com/k8sgpt-ai/k8sgpt/issues/1110)) ([eda5231](https://github.com/k8sgpt-ai/k8sgpt/commit/eda52312aef8113debbd770b8354c3a3cb1cc681))
|
||||
* oci genai ([#1102](https://github.com/k8sgpt-ai/k8sgpt/issues/1102)) ([047afd4](https://github.com/k8sgpt-ai/k8sgpt/commit/047afd46d62d1bd1da1435550cbaf9daaca53aee))
|
||||
* support AWS_PROFILE ([#1114](https://github.com/k8sgpt-ai/k8sgpt/issues/1114)) ([882c6f5](https://github.com/k8sgpt-ai/k8sgpt/commit/882c6f52252000da436e4fed9fd184b263f5a017))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to 0849a56 ([#1080](https://github.com/k8sgpt-ai/k8sgpt/issues/1080)) ([e894e77](https://github.com/k8sgpt-ai/k8sgpt/commit/e894e778e91d070448cd4a3f46dfc98dd588c9ed))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 to v2.19.1-20240406062209-1cc152efbf5c.1 ([#1070](https://github.com/k8sgpt-ai/k8sgpt/issues/1070)) ([24cff90](https://github.com/k8sgpt-ai/k8sgpt/commit/24cff90a0ca7488e48c94d13678529617c749aab))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20240406062209-1cc152efbf5c.3 ([#1086](https://github.com/k8sgpt-ai/k8sgpt/issues/1086)) ([820cd2e](https://github.com/k8sgpt-ai/k8sgpt/commit/820cd2e16cbca2c89b56a4d2a69f95f3f5cd6c6b))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.51.32 ([#1083](https://github.com/k8sgpt-ai/k8sgpt/issues/1083)) ([75c2add](https://github.com/k8sgpt-ai/k8sgpt/commit/75c2addf66a54df57d0c0ac17f0b359f7612e446))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.52.3 ([#1094](https://github.com/k8sgpt-ai/k8sgpt/issues/1094)) ([3c48231](https://github.com/k8sgpt-ai/k8sgpt/commit/3c4823127ca04d1d280da6d932e951e6c3f71536))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.5.2 ([#1084](https://github.com/k8sgpt-ai/k8sgpt/issues/1084)) ([bd695d0](https://github.com/k8sgpt-ai/k8sgpt/commit/bd695d0987e8ec12b44512c46bc5f2e5116076bd))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.3.2 ([#1085](https://github.com/k8sgpt-ai/k8sgpt/issues/1085)) ([43953ff](https://github.com/k8sgpt-ai/k8sgpt/commit/43953ffa3412ae97b6d54ed14b94955d1b73feba))
|
||||
* **deps:** update module github.com/cohere-ai/cohere-go/v2 to v2.7.3 ([#1087](https://github.com/k8sgpt-ai/k8sgpt/issues/1087)) ([36ccc62](https://github.com/k8sgpt-ai/k8sgpt/commit/36ccc628462ad102712fca115b56f521b2b33b38))
|
||||
* **deps:** update module github.com/google/generative-ai-go to v0.11.0 ([#1089](https://github.com/k8sgpt-ai/k8sgpt/issues/1089)) ([f30c9f5](https://github.com/k8sgpt-ai/k8sgpt/commit/f30c9f555449bb90bf8242b88b8fae936cb57938))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.23.0 ([#1091](https://github.com/k8sgpt-ai/k8sgpt/issues/1091)) ([e74fc08](https://github.com/k8sgpt-ai/k8sgpt/commit/e74fc0838feac5a019a340f7c5ad1c9ae49913fa))
|
||||
* **deps:** update module golang.org/x/net to v0.25.0 ([#1092](https://github.com/k8sgpt-ai/k8sgpt/issues/1092)) ([fe53907](https://github.com/k8sgpt-ai/k8sgpt/commit/fe53907c44e9cd56b6747f52ae3402bc6ae2bd49))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** pin codecov/codecov-action action to ab904c4 ([#1031](https://github.com/k8sgpt-ai/k8sgpt/issues/1031)) ([e0af76f](https://github.com/k8sgpt-ai/k8sgpt/commit/e0af76f3c9c0120dbc4d9373d69a262e1ec2b7f2))
|
||||
* **deps:** update actions/checkout digest to 0ad4b8f ([#1078](https://github.com/k8sgpt-ai/k8sgpt/issues/1078)) ([ea8183c](https://github.com/k8sgpt-ai/k8sgpt/commit/ea8183ce848ba58f91cfa68755d6f5b9cf695d36))
|
||||
* **deps:** update actions/upload-artifact digest to 6546280 ([#1079](https://github.com/k8sgpt-ai/k8sgpt/issues/1079)) ([9b797d7](https://github.com/k8sgpt-ai/k8sgpt/commit/9b797d7e8b4f704dae12acaa7778b6b65e2c36ac))
|
||||
* **deps:** update amannn/action-semantic-pull-request action to v5.5.2 ([#1088](https://github.com/k8sgpt-ai/k8sgpt/issues/1088)) ([a809a45](https://github.com/k8sgpt-ai/k8sgpt/commit/a809a455f55d1af104ebc0540007aa678581dd21))
|
||||
* **deps:** update anchore/sbom-action action to v0.15.11 ([#1082](https://github.com/k8sgpt-ai/k8sgpt/issues/1082)) ([12fa5ae](https://github.com/k8sgpt-ai/k8sgpt/commit/12fa5aef4dada597d7059e5717ec7bee3b38c122))
|
||||
* **deps:** update docker/build-push-action digest to 2cdde99 ([#1032](https://github.com/k8sgpt-ai/k8sgpt/issues/1032)) ([b12c006](https://github.com/k8sgpt-ai/k8sgpt/commit/b12c006c6304165269b90d770048b851e1aa1d1f))
|
||||
* **deps:** update google-github-actions/release-please-action action to v4.1.0 ([#1045](https://github.com/k8sgpt-ai/k8sgpt/issues/1045)) ([bf6f642](https://github.com/k8sgpt-ai/k8sgpt/commit/bf6f642c280f640f2c9020b325e52670ced2cf50))
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
* add logAnalyzer in README.md ([#1081](https://github.com/k8sgpt-ai/k8sgpt/issues/1081)) ([5cfe332](https://github.com/k8sgpt-ai/k8sgpt/commit/5cfe3325cb556cfb9d0532ae26727441c5177015))
|
||||
|
||||
## [0.3.30](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.29...v0.3.30) (2024-04-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add keda integration ([#1058](https://github.com/k8sgpt-ai/k8sgpt/issues/1058)) ([9a73d19](https://github.com/k8sgpt-ai/k8sgpt/commit/9a73d1923f146aa1343465d89225e64bcb8e0112))
|
||||
* add minio support ([#1048](https://github.com/k8sgpt-ai/k8sgpt/issues/1048)) ([e6085d4](https://github.com/k8sgpt-ai/k8sgpt/commit/e6085d4191a1695e295f4f6a2ac7219b67a37225))
|
||||
* add Resource Kind in output ([#1069](https://github.com/k8sgpt-ai/k8sgpt/issues/1069)) ([aa276a5](https://github.com/k8sgpt-ai/k8sgpt/commit/aa276a5379b3d24a8e7a1f8b1193832df5a46220))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to 4693a02 ([#1037](https://github.com/k8sgpt-ai/k8sgpt/issues/1037)) ([94cdce4](https://github.com/k8sgpt-ai/k8sgpt/commit/94cdce44b49e0bb85e8b541688b2206e7c1dc33d))
|
||||
* **deps:** update module cloud.google.com/go/storage to v1.39.1 ([#1029](https://github.com/k8sgpt-ai/k8sgpt/issues/1029)) ([a3896f4](https://github.com/k8sgpt-ai/k8sgpt/commit/a3896f4518ec6666a43de22a24a18f2b93c58073))
|
||||
* **deps:** update module cloud.google.com/go/storage to v1.40.0 ([#1054](https://github.com/k8sgpt-ai/k8sgpt/issues/1054)) ([6df0169](https://github.com/k8sgpt-ai/k8sgpt/commit/6df01694916504cc4af3795361a4285098e2de85))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.51.14 ([#1051](https://github.com/k8sgpt-ai/k8sgpt/issues/1051)) ([007b4bb](https://github.com/k8sgpt-ai/k8sgpt/commit/007b4bb8ec4b36705f76fd2f5d96464c75915573))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.51.21 ([#1056](https://github.com/k8sgpt-ai/k8sgpt/issues/1056)) ([ccb692c](https://github.com/k8sgpt-ai/k8sgpt/commit/ccb692c1fdc5496d9d5810dfe41dbf1bdeb68d00))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.51.8 ([#1046](https://github.com/k8sgpt-ai/k8sgpt/issues/1046)) ([19ae31b](https://github.com/k8sgpt-ai/k8sgpt/commit/19ae31b5dd5c54413025cee8081d112223e38400))
|
||||
* **deps:** update module github.com/google/generative-ai-go to v0.10.0 ([#1047](https://github.com/k8sgpt-ai/k8sgpt/issues/1047)) ([6b38a56](https://github.com/k8sgpt-ai/k8sgpt/commit/6b38a56afbdaa8e0d8f025088a52d3022673ef9d))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.20.4 ([#1039](https://github.com/k8sgpt-ai/k8sgpt/issues/1039)) ([6a46a26](https://github.com/k8sgpt-ai/k8sgpt/commit/6a46a26789f730d298cf49a706421f36bc8523b1))
|
||||
* **deps:** update module golang.org/x/net to v0.23.0 [security] ([#1071](https://github.com/k8sgpt-ai/k8sgpt/issues/1071)) ([693b23f](https://github.com/k8sgpt-ai/k8sgpt/commit/693b23f1fc33659a3c4f52fc4d9c23348b22bfb1))
|
||||
* invalid ParentObj in output ([#1068](https://github.com/k8sgpt-ai/k8sgpt/issues/1068)) ([b2ab943](https://github.com/k8sgpt-ai/k8sgpt/commit/b2ab94375e4233cdfa9762877995445c313bb962))
|
||||
* remove show password in auth list ([#1061](https://github.com/k8sgpt-ai/k8sgpt/issues/1061)) ([9e02637](https://github.com/k8sgpt-ai/k8sgpt/commit/9e0263778f6dbc179184fa9d86f07d808283d63e))
|
||||
* set topP from config ([#1053](https://github.com/k8sgpt-ai/k8sgpt/issues/1053)) ([c162cc2](https://github.com/k8sgpt-ai/k8sgpt/commit/c162cc22ee468070e0602d3fd684b022fa585c4f))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update anchore/sbom-action action to v0.15.10 ([#1044](https://github.com/k8sgpt-ai/k8sgpt/issues/1044)) ([e05a902](https://github.com/k8sgpt-ai/k8sgpt/commit/e05a902d904fc0b63998ae290f15e79d330317fb))
|
||||
* **deps:** update cohere client implementation to v2 ([#1062](https://github.com/k8sgpt-ai/k8sgpt/issues/1062)) ([eb7687a](https://github.com/k8sgpt-ai/k8sgpt/commit/eb7687a08917ad4048c6f00c17bb45591a935a3a))
|
||||
* **deps:** update docker/login-action digest to e92390c ([#1033](https://github.com/k8sgpt-ai/k8sgpt/issues/1033)) ([c872e49](https://github.com/k8sgpt-ai/k8sgpt/commit/c872e495ad6f787cf566a5b2f295deb3f08aba15))
|
||||
* **deps:** update docker/setup-buildx-action digest to 2b51285 ([#1036](https://github.com/k8sgpt-ai/k8sgpt/issues/1036)) ([10c00ba](https://github.com/k8sgpt-ai/k8sgpt/commit/10c00ba9fe61a3ee1dc90d87dd7997da276905b4))
|
||||
* **deps:** update docker/setup-buildx-action digest to d70bba7 ([#1066](https://github.com/k8sgpt-ai/k8sgpt/issues/1066)) ([3eaf776](https://github.com/k8sgpt-ai/k8sgpt/commit/3eaf776249719a0a13909d24e6b48deb6bf818b6))
|
||||
* update license file path to avoid conflicting installations ([#878](https://github.com/k8sgpt-ai/k8sgpt/issues/878)) ([#1073](https://github.com/k8sgpt-ai/k8sgpt/issues/1073)) ([85a76a3](https://github.com/k8sgpt-ai/k8sgpt/commit/85a76a3be06df0ff713192d1f08fd01d1e8f219b))
|
||||
* update renovate config and bundle deps in groups ([#1026](https://github.com/k8sgpt-ai/k8sgpt/issues/1026)) ([bd2e06b](https://github.com/k8sgpt-ai/k8sgpt/commit/bd2e06bae72528c5af1b4f44674d624d474d40dc))
|
||||
|
||||
|
||||
### Refactoring
|
||||
|
||||
* replace util.SliceContainsString with slices.Contains & make fmt ([#1041](https://github.com/k8sgpt-ai/k8sgpt/issues/1041)) ([1ae4e75](https://github.com/k8sgpt-ai/k8sgpt/commit/1ae4e751967850e8146f8f3fa04c0dd302ef15bf))
|
||||
|
||||
## [0.3.29](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.28...v0.3.29) (2024-03-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* codecov ([#1023](https://github.com/k8sgpt-ai/k8sgpt/issues/1023)) ([fe81d16](https://github.com/k8sgpt-ai/k8sgpt/commit/fe81d16f756e5ea9db909e42e6caf1e17e040f86))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* allows an environmental override of the default AWS region and… ([#1025](https://github.com/k8sgpt-ai/k8sgpt/issues/1025)) ([8f8f5c6](https://github.com/k8sgpt-ai/k8sgpt/commit/8f8f5c6df7fbcd08ee48d91a4f2e011a3e69e4ac))
|
||||
|
||||
## [0.3.28](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.27...v0.3.28) (2024-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Google Vertex AI as provider to utilize gemini via GCP ([#984](https://github.com/k8sgpt-ai/k8sgpt/issues/984)) ([55ac0b2](https://github.com/k8sgpt-ai/k8sgpt/commit/55ac0b2129a438661a0253251f546db6b59f2b92))
|
||||
* add proxysettings for azureopenai and openai ([#987](https://github.com/k8sgpt-ai/k8sgpt/issues/987)) ([307710e](https://github.com/k8sgpt-ai/k8sgpt/commit/307710eddc1c3f96f40a674f7dda786510e9c4cc))
|
||||
* aws integration ([#967](https://github.com/k8sgpt-ai/k8sgpt/issues/967)) ([a81377f](https://github.com/k8sgpt-ai/k8sgpt/commit/a81377f72db7f322e0afbb6d613c2bfffecf8080))
|
||||
* enable Rest api using grpc-gateway ([#834](https://github.com/k8sgpt-ai/k8sgpt/issues/834)) ([f2138c7](https://github.com/k8sgpt-ai/k8sgpt/commit/f2138c71017b391625eebdfb4c5708c824824f69))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* analyze command default backend bug ([#966](https://github.com/k8sgpt-ai/k8sgpt/issues/966)) ([aab8d77](https://github.com/k8sgpt-ai/k8sgpt/commit/aab8d77febdd4b42ff74aafbb2ada27745c04ae1))
|
||||
* **deps:** update module cloud.google.com/go/storage to v1.38.0 ([#950](https://github.com/k8sgpt-ai/k8sgpt/issues/950)) ([6207c70](https://github.com/k8sgpt-ai/k8sgpt/commit/6207c70c51d2885c4590c255c8f78e7ee2009034))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.50.20 ([#930](https://github.com/k8sgpt-ai/k8sgpt/issues/930)) ([3f0356b](https://github.com/k8sgpt-ai/k8sgpt/commit/3f0356be662c32d82ce4f3db05f859477823717d))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.50.21 ([#970](https://github.com/k8sgpt-ai/k8sgpt/issues/970)) ([00c91f0](https://github.com/k8sgpt-ai/k8sgpt/commit/00c91f05a62b2c8b2d756b58b95279195ff38d3d))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.50.22 ([#971](https://github.com/k8sgpt-ai/k8sgpt/issues/971)) ([6ac815c](https://github.com/k8sgpt-ai/k8sgpt/commit/6ac815c10fb073f4251e338ab22e247625f21406))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.50.34 ([#974](https://github.com/k8sgpt-ai/k8sgpt/issues/974)) ([425f33b](https://github.com/k8sgpt-ai/k8sgpt/commit/425f33bb2ddf8cdaff079b097d6956f675c89b0e))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.3.1 ([#992](https://github.com/k8sgpt-ai/k8sgpt/issues/992)) ([85f18dd](https://github.com/k8sgpt-ai/k8sgpt/commit/85f18dde1f820fe2413cc6b3109e67b7a010142c))
|
||||
* **deps:** update module github.com/google/generative-ai-go to v0.8.0 ([#965](https://github.com/k8sgpt-ai/k8sgpt/issues/965)) ([248260e](https://github.com/k8sgpt-ai/k8sgpt/commit/248260e081327de9f9d1d2c851efab2b4a3e7ede))
|
||||
* **deps:** update module github.com/prometheus/client_golang to v1.19.0 ([#989](https://github.com/k8sgpt-ai/k8sgpt/issues/989)) ([4065fae](https://github.com/k8sgpt-ai/k8sgpt/commit/4065faef13691f9cf1f50696c62d3b30b0933b4b))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.19.4 ([#963](https://github.com/k8sgpt-ai/k8sgpt/issues/963)) ([8b0b61e](https://github.com/k8sgpt-ai/k8sgpt/commit/8b0b61e596f790b9558a5e3d1f634a5ee1c6cb0c))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.20.0 ([#977](https://github.com/k8sgpt-ai/k8sgpt/issues/977)) ([e07822c](https://github.com/k8sgpt-ai/k8sgpt/commit/e07822c10bff5dbd91f4da592914c25538353d6b))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.20.1 ([#986](https://github.com/k8sgpt-ai/k8sgpt/issues/986)) ([88a7907](https://github.com/k8sgpt-ai/k8sgpt/commit/88a7907db4700c241e9aa109bc3d8604a8186f87))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.20.2 ([#991](https://github.com/k8sgpt-ai/k8sgpt/issues/991)) ([d2754d3](https://github.com/k8sgpt-ai/k8sgpt/commit/d2754d320fb1f285f93fdced2b8469280bd47fd2))
|
||||
* **deps:** update module github.com/schollz/progressbar/v3 to v3.14.2 ([#983](https://github.com/k8sgpt-ai/k8sgpt/issues/983)) ([af3732a](https://github.com/k8sgpt-ai/k8sgpt/commit/af3732ad067b809c54c5f08f6cf5a7a519b452d7))
|
||||
* **deps:** update module github.com/stretchr/testify to v1.9.0 ([#999](https://github.com/k8sgpt-ai/k8sgpt/issues/999)) ([1491e67](https://github.com/k8sgpt-ai/k8sgpt/commit/1491e675673dcc13ccf6ac1778113762542e8cbc))
|
||||
* **deps:** update module go.uber.org/zap to v1.27.0 ([#972](https://github.com/k8sgpt-ai/k8sgpt/issues/972)) ([8f00218](https://github.com/k8sgpt-ai/k8sgpt/commit/8f002180901c8bf7e6b1a5451dd97ef566260b0f))
|
||||
* **deps:** update module google.golang.org/api to v0.165.0 ([#959](https://github.com/k8sgpt-ai/k8sgpt/issues/959)) ([cc99bd5](https://github.com/k8sgpt-ai/k8sgpt/commit/cc99bd51f05db4e87f806ac58ee1cb7a83b25e4d))
|
||||
* **deps:** update module google.golang.org/api to v0.167.0 ([#973](https://github.com/k8sgpt-ai/k8sgpt/issues/973)) ([6103c96](https://github.com/k8sgpt-ai/k8sgpt/commit/6103c96c41e10e2fe13d285ff15a36bf2fbeb5c2))
|
||||
* **deps:** update module google.golang.org/grpc to v1.62.0 ([#975](https://github.com/k8sgpt-ai/k8sgpt/issues/975)) ([97446aa](https://github.com/k8sgpt-ai/k8sgpt/commit/97446aae079824d6556416314c0a27514088a667))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#957](https://github.com/k8sgpt-ai/k8sgpt/issues/957)) ([f929e7f](https://github.com/k8sgpt-ai/k8sgpt/commit/f929e7feea5931ddec77af49dd08937aca85fd49))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#979](https://github.com/k8sgpt-ai/k8sgpt/issues/979)) ([35f5185](https://github.com/k8sgpt-ai/k8sgpt/commit/35f51859140c78ce953443afcc27f77230287809))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#980](https://github.com/k8sgpt-ai/k8sgpt/issues/980)) ([334a86a](https://github.com/k8sgpt-ai/k8sgpt/commit/334a86aaf40e5421929cf380191841db064d9bf7))
|
||||
* log analyzer failed with multiple containers in the pod ([#920](https://github.com/k8sgpt-ai/k8sgpt/issues/920)) ([98286a9](https://github.com/k8sgpt-ai/k8sgpt/commit/98286a965e4c4c680deeb43d3397b51089968366))
|
||||
* set result name and namespace to trivy vulnreport and configaudi… ([#869](https://github.com/k8sgpt-ai/k8sgpt/issues/869)) ([a3cd7e6](https://github.com/k8sgpt-ai/k8sgpt/commit/a3cd7e6385365a1d190a9e8439311cb9d5eeda56))
|
||||
* shorthand for the http flag in serve command ([#969](https://github.com/k8sgpt-ai/k8sgpt/issues/969)) ([f55f837](https://github.com/k8sgpt-ai/k8sgpt/commit/f55f8370ebf0db6db629641337cd78ad7f120865))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* attempt to group renovate deps ([#1007](https://github.com/k8sgpt-ai/k8sgpt/issues/1007)) ([adf4f17](https://github.com/k8sgpt-ai/k8sgpt/commit/adf4f17085672fd5ae78dad4f8ac1d887029836d))
|
||||
* **deps:** update anchore/sbom-action action to v0.15.9 ([#1004](https://github.com/k8sgpt-ai/k8sgpt/issues/1004)) ([b05b6a3](https://github.com/k8sgpt-ai/k8sgpt/commit/b05b6a38ed4a9fc017f9dcb52cff8a332c11056d))
|
||||
* **deps:** update docker/build-push-action digest to af5a7ed ([#1003](https://github.com/k8sgpt-ai/k8sgpt/issues/1003)) ([b58b719](https://github.com/k8sgpt-ai/k8sgpt/commit/b58b7191af2fe082d94d46ef6a2784c1ea322340))
|
||||
* **deps:** update docker/setup-buildx-action digest to 0d103c3 ([#988](https://github.com/k8sgpt-ai/k8sgpt/issues/988)) ([f24bcd8](https://github.com/k8sgpt-ai/k8sgpt/commit/f24bcd88b6a915798897b49a562b86265a9b524c))
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to 00311c2 ([#1002](https://github.com/k8sgpt-ai/k8sgpt/issues/1002)) ([4ec143a](https://github.com/k8sgpt-ai/k8sgpt/commit/4ec143ab772ca4dc3072c248e95da8f7c0a2974b))
|
||||
|
||||
## [0.3.27](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.26...v0.3.27) (2024-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add huggingface provider ([#893](https://github.com/k8sgpt-ai/k8sgpt/issues/893)) ([2fd476e](https://github.com/k8sgpt-ai/k8sgpt/commit/2fd476e12624e30570c0819594f2668f720381d6))
|
||||
* added FailedMount event reason to get the failure ([#883](https://github.com/k8sgpt-ai/k8sgpt/issues/883)) ([78126b2](https://github.com/k8sgpt-ai/k8sgpt/commit/78126b2328c1b3f81a269d203e86128104050010))
|
||||
* enables remote custom analyzers ([#906](https://github.com/k8sgpt-ai/k8sgpt/issues/906)) ([c8c9dbf](https://github.com/k8sgpt-ai/k8sgpt/commit/c8c9dbfadc72a193ab9f3431d02d50ac5ab5d071))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to e7106e6 ([#897](https://github.com/k8sgpt-ai/k8sgpt/issues/897)) ([28c4c57](https://github.com/k8sgpt-ai/k8sgpt/commit/28c4c57e4566b9b888a5633090ccb70875d30106))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20240128172516-6bf6a55ff115.2 ([#899](https://github.com/k8sgpt-ai/k8sgpt/issues/899)) ([e3eee6d](https://github.com/k8sgpt-ai/k8sgpt/commit/e3eee6d9566a59fd62e6bb804257b1383f75e3ef))
|
||||
* **deps:** update module cloud.google.com/go/storage to v1.37.0 ([#934](https://github.com/k8sgpt-ai/k8sgpt/issues/934)) ([3d2554b](https://github.com/k8sgpt-ai/k8sgpt/commit/3d2554b9cd8817b24cf8858a107420d6d8424aa4))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.21 ([#868](https://github.com/k8sgpt-ai/k8sgpt/issues/868)) ([88002e7](https://github.com/k8sgpt-ai/k8sgpt/commit/88002e7e8c3e9c71365c44e136a6f1a8d35e1744))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.50.2 ([#887](https://github.com/k8sgpt-ai/k8sgpt/issues/887)) ([817d9cf](https://github.com/k8sgpt-ai/k8sgpt/commit/817d9cf754d307d374befc0d57919eb7a0183aaf))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.5.1 ([#939](https://github.com/k8sgpt-ai/k8sgpt/issues/939)) ([ce7c955](https://github.com/k8sgpt-ai/k8sgpt/commit/ce7c9551bcb1a8b24922a1eb062605bbfeec7929))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.3.0 ([#952](https://github.com/k8sgpt-ai/k8sgpt/issues/952)) ([fea2ed1](https://github.com/k8sgpt-ai/k8sgpt/commit/fea2ed1fff5fb5a46d6abc2feb72e1e1adf3b69b))
|
||||
* **deps:** update module github.com/google/generative-ai-go to v0.7.0 ([#940](https://github.com/k8sgpt-ai/k8sgpt/issues/940)) ([3c8d9d4](https://github.com/k8sgpt-ai/k8sgpt/commit/3c8d9d42e573f27185a1572d1bc06f8af87f3a0b))
|
||||
* **deps:** update module github.com/prometheus/prometheus to v2 ([#863](https://github.com/k8sgpt-ai/k8sgpt/issues/863)) ([a253af2](https://github.com/k8sgpt-ai/k8sgpt/commit/a253af23b601b23179be5019fbb832a41423cdae))
|
||||
* **deps:** update module github.com/pterm/pterm to v0.12.75 ([#881](https://github.com/k8sgpt-ai/k8sgpt/issues/881)) ([e7d690a](https://github.com/k8sgpt-ai/k8sgpt/commit/e7d690afd12cb71d7b344ba92bf059ae18a993c8))
|
||||
* **deps:** update module github.com/pterm/pterm to v0.12.78 ([#890](https://github.com/k8sgpt-ai/k8sgpt/issues/890)) ([f9c1b90](https://github.com/k8sgpt-ai/k8sgpt/commit/f9c1b903385978be56f9c4bc87089bd1c761bbea))
|
||||
* **deps:** update module github.com/pterm/pterm to v0.12.79 ([#943](https://github.com/k8sgpt-ai/k8sgpt/issues/943)) ([bfbb5c7](https://github.com/k8sgpt-ai/k8sgpt/commit/bfbb5c7e03cad144f6037c7233ffc0817fd403e4))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.18.1 ([#871](https://github.com/k8sgpt-ai/k8sgpt/issues/871)) ([6c62c1a](https://github.com/k8sgpt-ai/k8sgpt/commit/6c62c1a0fcd38cf9de8a99cda6f37b221740b9c8))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.18.2 ([#874](https://github.com/k8sgpt-ai/k8sgpt/issues/874)) ([4de1bbd](https://github.com/k8sgpt-ai/k8sgpt/commit/4de1bbd6f72ca83d46ce5955bac50dffc99af03d))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.19.2 ([#886](https://github.com/k8sgpt-ai/k8sgpt/issues/886)) ([c601972](https://github.com/k8sgpt-ai/k8sgpt/commit/c6019728aea837884620e0b4894568802a948a6e))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.19.3 ([#937](https://github.com/k8sgpt-ai/k8sgpt/issues/937)) ([f2eb1ef](https://github.com/k8sgpt-ai/k8sgpt/commit/f2eb1ef5334877fd3a26dda8c92023f831ea857e))
|
||||
* **deps:** update module golang.org/x/term to v0.17.0 ([#941](https://github.com/k8sgpt-ai/k8sgpt/issues/941)) ([4e57088](https://github.com/k8sgpt-ai/k8sgpt/commit/4e57088a0137767a42c778a59ff07fff04c04289))
|
||||
* **deps:** update module google.golang.org/api to v0.157.0 ([#860](https://github.com/k8sgpt-ai/k8sgpt/issues/860)) ([72e08ef](https://github.com/k8sgpt-ai/k8sgpt/commit/72e08efff1fc501dfcba791c9d940e575f3e2395))
|
||||
* **deps:** update module google.golang.org/api to v0.164.0 ([#953](https://github.com/k8sgpt-ai/k8sgpt/issues/953)) ([29b482f](https://github.com/k8sgpt-ai/k8sgpt/commit/29b482f5978795fa8db729030bd75803e2e61f95))
|
||||
* **deps:** update module google.golang.org/grpc to v1.61.1 ([#954](https://github.com/k8sgpt-ai/k8sgpt/issues/954)) ([9c1f1b8](https://github.com/k8sgpt-ai/k8sgpt/commit/9c1f1b8804a26f549379efe637d0bedb8e2cb890))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#866](https://github.com/k8sgpt-ai/k8sgpt/issues/866)) ([81d6604](https://github.com/k8sgpt-ai/k8sgpt/commit/81d660447d236cd03b75866871bb69f2c77c5c66))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#875](https://github.com/k8sgpt-ai/k8sgpt/issues/875)) ([1f371e2](https://github.com/k8sgpt-ai/k8sgpt/commit/1f371e2807c47dbb4613bf873ec67a77e8e6c80c))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#956](https://github.com/k8sgpt-ai/k8sgpt/issues/956)) ([d9fe744](https://github.com/k8sgpt-ai/k8sgpt/commit/d9fe7446af428209610adc83ec17cf50491a5a47))
|
||||
* lint errors ([#923](https://github.com/k8sgpt-ai/k8sgpt/issues/923)) ([3415031](https://github.com/k8sgpt-ai/k8sgpt/commit/3415031006bb5899019e68d33ac6083d03ef864b))
|
||||
* typo in httproute files name ([#877](https://github.com/k8sgpt-ai/k8sgpt/issues/877)) ([cdbeb14](https://github.com/k8sgpt-ai/k8sgpt/commit/cdbeb146a28ebc21ac2c4d27e977b1771f9290b4))
|
||||
* unused variable failure warning in webhooks file ([#916](https://github.com/k8sgpt-ai/k8sgpt/issues/916)) ([3f0964a](https://github.com/k8sgpt-ai/k8sgpt/commit/3f0964ad385390f53516904219fbfc47b989d31f))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/upload-artifact digest to 26f96df ([#888](https://github.com/k8sgpt-ai/k8sgpt/issues/888)) ([483a9da](https://github.com/k8sgpt-ai/k8sgpt/commit/483a9dad103ad1af82491dc1d5e0a39bb4865a1b))
|
||||
* **deps:** update actions/upload-artifact digest to 5d5d22a ([#925](https://github.com/k8sgpt-ai/k8sgpt/issues/925)) ([070aa7f](https://github.com/k8sgpt-ai/k8sgpt/commit/070aa7fdd0982c0c7f02a1da9e6797d5efaa5586))
|
||||
* **deps:** update actions/upload-artifact digest to 694cdab ([#880](https://github.com/k8sgpt-ai/k8sgpt/issues/880)) ([3cf18e7](https://github.com/k8sgpt-ai/k8sgpt/commit/3cf18e783edb341b7bdd6aa20dbcce11971fa241))
|
||||
* **deps:** update anchore/sbom-action action to v0.15.4 ([#879](https://github.com/k8sgpt-ai/k8sgpt/issues/879)) ([d213399](https://github.com/k8sgpt-ai/k8sgpt/commit/d2133991617697b13b8846f2acb3a3bb6cebb160))
|
||||
* **deps:** update anchore/sbom-action action to v0.15.5 ([#885](https://github.com/k8sgpt-ai/k8sgpt/issues/885)) ([60853fe](https://github.com/k8sgpt-ai/k8sgpt/commit/60853fe4eb8de7a1fdbaea388c3d2d6205e273a6))
|
||||
* **deps:** update anchore/sbom-action action to v0.15.8 ([#926](https://github.com/k8sgpt-ai/k8sgpt/issues/926)) ([f61c3e2](https://github.com/k8sgpt-ai/k8sgpt/commit/f61c3e228c69fa160735ddb2c1347720112b738f))
|
||||
* **deps:** update golang docker tag to v1.22 ([#931](https://github.com/k8sgpt-ai/k8sgpt/issues/931)) ([37228d8](https://github.com/k8sgpt-ai/k8sgpt/commit/37228d88e357c66c5574559ae27a52fdf28418b8))
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to 8e1117c ([#915](https://github.com/k8sgpt-ai/k8sgpt/issues/915)) ([599be33](https://github.com/k8sgpt-ai/k8sgpt/commit/599be33f38ad1fd688b8e7824102a7944d516435))
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to f016e79 ([#714](https://github.com/k8sgpt-ai/k8sgpt/issues/714)) ([335616c](https://github.com/k8sgpt-ai/k8sgpt/commit/335616c20f7f8d9fefab4976d986a8d3b4867111))
|
||||
* grpc update ([#938](https://github.com/k8sgpt-ai/k8sgpt/issues/938)) ([bbf61f5](https://github.com/k8sgpt-ai/k8sgpt/commit/bbf61f53d4fb9244b5a79ae953370296ca9fd44b))
|
||||
* improve codebase and doc quality ([#922](https://github.com/k8sgpt-ai/k8sgpt/issues/922)) ([d97dea2](https://github.com/k8sgpt-ai/k8sgpt/commit/d97dea289681cd061ca0796208c50720bdb08914))
|
||||
* linting improvements and catching false positives ([#882](https://github.com/k8sgpt-ai/k8sgpt/issues/882)) ([2effbb3](https://github.com/k8sgpt-ai/k8sgpt/commit/2effbb345ad1c2771ec798e06ccde68d3253b4bc))
|
||||
* set correct license during package build ([#872](https://github.com/k8sgpt-ai/k8sgpt/issues/872)) ([42be51b](https://github.com/k8sgpt-ai/k8sgpt/commit/42be51bc8f625a35b1435c461d9a32c3c4905f1c))
|
||||
* updated deps ([#951](https://github.com/k8sgpt-ai/k8sgpt/issues/951)) ([015bccf](https://github.com/k8sgpt-ai/k8sgpt/commit/015bccfc2eae587e0ade371211404f5af4c37d27))
|
||||
|
||||
## [0.3.26](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.25...v0.3.26) (2024-01-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* initial Prometheus analyzers ([#855](https://github.com/k8sgpt-ai/k8sgpt/issues/855)) ([45fa827](https://github.com/k8sgpt-ai/k8sgpt/commit/45fa827c046b91d901a08bec1a892d9c0917f350))
|
||||
* interactive mode ([#854](https://github.com/k8sgpt-ai/k8sgpt/issues/854)) ([9da75e0](https://github.com/k8sgpt-ai/k8sgpt/commit/9da75e02bc17146898377e4f90b7f59c5a8e0eee))
|
||||
* unify aiClientName const for all providers ([#848](https://github.com/k8sgpt-ai/k8sgpt/issues/848)) ([5c17c24](https://github.com/k8sgpt-ai/k8sgpt/commit/5c17c240550609d9fb7771fe67fe1ab19660b4da))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.16 ([#847](https://github.com/k8sgpt-ai/k8sgpt/issues/847)) ([ce4910b](https://github.com/k8sgpt-ai/k8sgpt/commit/ce4910bc5d064f80076877d7a096fff903308b63))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.17 ([#852](https://github.com/k8sgpt-ai/k8sgpt/issues/852)) ([85ebd12](https://github.com/k8sgpt-ai/k8sgpt/commit/85ebd12c30d369c5ef9a42b5a834d091523a7b6e))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.18 ([#856](https://github.com/k8sgpt-ai/k8sgpt/issues/856)) ([4106d39](https://github.com/k8sgpt-ai/k8sgpt/commit/4106d39c322940413ebfd9ac0bf6f5bd31830e93))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.19 ([#859](https://github.com/k8sgpt-ai/k8sgpt/issues/859)) ([6a2f315](https://github.com/k8sgpt-ai/k8sgpt/commit/6a2f315b2f4344f2924b7915e8a1393f9732a1e9))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.17.11 ([#853](https://github.com/k8sgpt-ai/k8sgpt/issues/853)) ([1979c86](https://github.com/k8sgpt-ai/k8sgpt/commit/1979c86d0f59921d55cd4229a37d604a6f1dc578))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.17.11 ([#861](https://github.com/k8sgpt-ai/k8sgpt/issues/861)) ([40b5b7e](https://github.com/k8sgpt-ai/k8sgpt/commit/40b5b7e185c8d335bdefb131988b9900ad26bac3))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#864](https://github.com/k8sgpt-ai/k8sgpt/issues/864)) ([36ba6c5](https://github.com/k8sgpt-ai/k8sgpt/commit/36ba6c5147a9ed75c14dbba4bc06cae903e651a4))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#865](https://github.com/k8sgpt-ai/k8sgpt/issues/865)) ([c55025d](https://github.com/k8sgpt-ai/k8sgpt/commit/c55025d04ebf9da0f6092aabb0b043ccef05164c))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/upload-artifact digest to 1eb3cb2 ([#867](https://github.com/k8sgpt-ai/k8sgpt/issues/867)) ([4ce56f3](https://github.com/k8sgpt-ai/k8sgpt/commit/4ce56f38b4338a6a2fe69f588b0f17e0b54d0ae6))
|
||||
* **deps:** update anchore/sbom-action action to v0.15.3 ([#850](https://github.com/k8sgpt-ai/k8sgpt/issues/850)) ([12f764d](https://github.com/k8sgpt-ai/k8sgpt/commit/12f764d5846accbd987d40f69a153dceb9954f39))
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
* adjusted README information about providers ([#844](https://github.com/k8sgpt-ai/k8sgpt/issues/844)) ([745e960](https://github.com/k8sgpt-ai/k8sgpt/commit/745e960f492e6dd0e50aa4a1ce7239c677025024))
|
||||
|
||||
## [0.3.25](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.24...v0.3.25) (2024-01-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added Google GenAI client; simplified IAI/clients API surface. ([#829](https://github.com/k8sgpt-ai/k8sgpt/issues/829)) ([e7d4149](https://github.com/k8sgpt-ai/k8sgpt/commit/e7d41496ddaa145c70079852da8b2ce3b3b7289f))
|
||||
* code_cov badge ([#821](https://github.com/k8sgpt-ai/k8sgpt/issues/821)) ([fcd29a5](https://github.com/k8sgpt-ai/k8sgpt/commit/fcd29a547d73ba48935762e2f568f5755f5c6ed3))
|
||||
* coverage reports ([#819](https://github.com/k8sgpt-ai/k8sgpt/issues/819)) ([3d0ba3e](https://github.com/k8sgpt-ai/k8sgpt/commit/3d0ba3e78cabaf5f1262c5b5b16ebabad974fa87))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.10 ([#811](https://github.com/k8sgpt-ai/k8sgpt/issues/811)) ([e5cc4a2](https://github.com/k8sgpt-ai/k8sgpt/commit/e5cc4a28cb3682e7094e6ceddf91b65da991ddb6))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.12 ([#813](https://github.com/k8sgpt-ai/k8sgpt/issues/813)) ([91613ba](https://github.com/k8sgpt-ai/k8sgpt/commit/91613baa5cc5244c93deb344abcdd905802eef30))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.14 ([#822](https://github.com/k8sgpt-ai/k8sgpt/issues/822)) ([526e22f](https://github.com/k8sgpt-ai/k8sgpt/commit/526e22f88b8de15eceb10965b045ef0366ff2d6c))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.15 ([#835](https://github.com/k8sgpt-ai/k8sgpt/issues/835)) ([e78ff05](https://github.com/k8sgpt-ai/k8sgpt/commit/e78ff054190cd54cabe17d77ac69443e517f1e55))
|
||||
* **deps:** update module github.com/prometheus/client_golang to v1.18.0 ([#814](https://github.com/k8sgpt-ai/k8sgpt/issues/814)) ([6eb8f67](https://github.com/k8sgpt-ai/k8sgpt/commit/6eb8f6793ed989ba3ac7ed00336345f68b09bf45))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.17.10 ([#824](https://github.com/k8sgpt-ai/k8sgpt/issues/824)) ([4314804](https://github.com/k8sgpt-ai/k8sgpt/commit/4314804ca7e782f5149dc2078ba9c859edc4688a))
|
||||
* **deps:** update module golang.org/x/term to v0.16.0 ([#831](https://github.com/k8sgpt-ai/k8sgpt/issues/831)) ([4de989c](https://github.com/k8sgpt-ai/k8sgpt/commit/4de989c803ee43a02d75112d1b3a54daee3dd9af))
|
||||
* **deps:** update module google.golang.org/api to v0.155.0 ([#836](https://github.com/k8sgpt-ai/k8sgpt/issues/836)) ([105a239](https://github.com/k8sgpt-ai/k8sgpt/commit/105a239d94384f4096c01d9978564040773ab56e))
|
||||
* no explain case, improved readability. ([#825](https://github.com/k8sgpt-ai/k8sgpt/issues/825)) ([035348d](https://github.com/k8sgpt-ai/k8sgpt/commit/035348d8a0d290ac26b42425945eaafe038cedc5))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* added basic server startup test ([#817](https://github.com/k8sgpt-ai/k8sgpt/issues/817)) ([3e7cea7](https://github.com/k8sgpt-ai/k8sgpt/commit/3e7cea7bd39253718bc3d2f8b10ac5fc9b98cbc2))
|
||||
* **deps:** pin codecov/codecov-action action to eaaf4be ([#820](https://github.com/k8sgpt-ai/k8sgpt/issues/820)) ([2f0f2df](https://github.com/k8sgpt-ai/k8sgpt/commit/2f0f2dfa8a5957cb8b10864c14d7883158723a6a))
|
||||
* **deps:** update anchore/sbom-action action to v0.15.2 ([#823](https://github.com/k8sgpt-ai/k8sgpt/issues/823)) ([70c6892](https://github.com/k8sgpt-ai/k8sgpt/commit/70c68929d8d963c0bd17390c76e366d4339f56b9))
|
||||
* lint fixes ([#833](https://github.com/k8sgpt-ai/k8sgpt/issues/833)) ([a7e9b48](https://github.com/k8sgpt-ai/k8sgpt/commit/a7e9b486bad7c2d62878e470a755d1fef3803680))
|
||||
* remove code cov ([#832](https://github.com/k8sgpt-ai/k8sgpt/issues/832)) ([a774265](https://github.com/k8sgpt-ai/k8sgpt/commit/a77426593d7f3a8cfa810336ff08a2266db7fb4f))
|
||||
|
||||
|
||||
### Dependency Updates
|
||||
|
||||
* go module bump to fix CVE: GHSA-45x7-px36-x8w8 & GHSA-7ww5-4wqc-m92c ([#810](https://github.com/k8sgpt-ai/k8sgpt/issues/810)) ([b17fd7c](https://github.com/k8sgpt-ai/k8sgpt/commit/b17fd7c98644afa70d414fcb32e49e61e1c831ad))
|
||||
|
||||
## [0.3.24](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.23...v0.3.24) (2023-12-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add last termination state when pod is in CrashloopBackoff ([#792](https://github.com/k8sgpt-ai/k8sgpt/issues/792)) ([ff4aaf7](https://github.com/k8sgpt-ai/k8sgpt/commit/ff4aaf7c328a58fcad8e4fb0f93ea543725eedd5))
|
||||
* Add license scan report and status ([#796](https://github.com/k8sgpt-ai/k8sgpt/issues/796)) ([343aec8](https://github.com/k8sgpt-ai/k8sgpt/commit/343aec8f0455c9461eb8d495ca5bd446b4bad667))
|
||||
* version upgrade to 1.21 ([#798](https://github.com/k8sgpt-ai/k8sgpt/issues/798)) ([c23f24d](https://github.com/k8sgpt-ai/k8sgpt/commit/c23f24de2e79347e4f5465e28af34e138cc13231))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* added the ability to set the trivy variables by the user ([#797](https://github.com/k8sgpt-ai/k8sgpt/issues/797)) ([928b39a](https://github.com/k8sgpt-ai/k8sgpt/commit/928b39a7283ee274dd517e727624eceb3795594d))
|
||||
* **deps:** update module cloud.google.com/go/storage to v1.36.0 ([#805](https://github.com/k8sgpt-ai/k8sgpt/issues/805)) ([390f309](https://github.com/k8sgpt-ai/k8sgpt/commit/390f30908800dfe21e2c1660139b0bd9d36b34d6))
|
||||
* **deps:** update module github.com/aquasecurity/trivy-operator to v0.17.1 ([#780](https://github.com/k8sgpt-ai/k8sgpt/issues/780)) ([71f36bd](https://github.com/k8sgpt-ai/k8sgpt/commit/71f36bdb0b3729c4357299b7d03829dd5b6a69ec))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.6 ([#783](https://github.com/k8sgpt-ai/k8sgpt/issues/783)) ([1b386f6](https://github.com/k8sgpt-ai/k8sgpt/commit/1b386f64f2863d8a49f423ad571cba009807bc55))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.7 ([#804](https://github.com/k8sgpt-ai/k8sgpt/issues/804)) ([3c6c759](https://github.com/k8sgpt-ai/k8sgpt/commit/3c6c7597e014bfd68794b1764c3a8902e8a798ea))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.8 ([#807](https://github.com/k8sgpt-ai/k8sgpt/issues/807)) ([93b5ca1](https://github.com/k8sgpt-ai/k8sgpt/commit/93b5ca1985c3730592388ba6fc32ecca9b806888))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.49.9 ([#808](https://github.com/k8sgpt-ai/k8sgpt/issues/808)) ([130e4c2](https://github.com/k8sgpt-ai/k8sgpt/commit/130e4c2efd0e5b34cdc84c357c6c1f3987cf7c35))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.2.1 ([#801](https://github.com/k8sgpt-ai/k8sgpt/issues/801)) ([aa05756](https://github.com/k8sgpt-ai/k8sgpt/commit/aa057565b5c971c493443f3ede4aed8f8a6399f7))
|
||||
* **deps:** update module github.com/mittwald/go-helm-client to v0.12.5 ([#802](https://github.com/k8sgpt-ai/k8sgpt/issues/802)) ([4a7bad3](https://github.com/k8sgpt-ai/k8sgpt/commit/4a7bad313b66750bd830413b7fef005580ad843c))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.17.9 ([#772](https://github.com/k8sgpt-ai/k8sgpt/issues/772)) ([13d64a5](https://github.com/k8sgpt-ai/k8sgpt/commit/13d64a58750c7262c07042b557fbf2c4a511b777))
|
||||
* **deps:** update module github.com/spf13/viper to v1.18.2 ([#787](https://github.com/k8sgpt-ai/k8sgpt/issues/787)) ([8dea617](https://github.com/k8sgpt-ai/k8sgpt/commit/8dea6170a2c00c03f08f25e4f0a232be617536f1))
|
||||
* **deps:** update module google.golang.org/api to v0.154.0 ([#779](https://github.com/k8sgpt-ai/k8sgpt/issues/779)) ([78f7f2b](https://github.com/k8sgpt-ai/k8sgpt/commit/78f7f2ba85fd357cab13ccc15e9e767e8611773a))
|
||||
* **deps:** update module google.golang.org/grpc to v1.60.1 ([#790](https://github.com/k8sgpt-ai/k8sgpt/issues/790)) ([5d54c3f](https://github.com/k8sgpt-ai/k8sgpt/commit/5d54c3f840a9ce002606b6601187e69fb62f8a28))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.13.3 ([#803](https://github.com/k8sgpt-ai/k8sgpt/issues/803)) ([a8e1932](https://github.com/k8sgpt-ai/k8sgpt/commit/a8e193212222811f3a278df6056dd2165c4323bd))
|
||||
* lowercase logs before running regex matching in LogAnalyzer ([#794](https://github.com/k8sgpt-ai/k8sgpt/issues/794)) ([03b63be](https://github.com/k8sgpt-ai/k8sgpt/commit/03b63befa247ac84b795a0ec8d5280196b8d570d))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/setup-go action to v5 ([#788](https://github.com/k8sgpt-ai/k8sgpt/issues/788)) ([d00ed33](https://github.com/k8sgpt-ai/k8sgpt/commit/d00ed33678b1560a3996f1d735d84ca0ca05c0b0))
|
||||
* **deps:** update actions/upload-artifact action to v4 ([#806](https://github.com/k8sgpt-ai/k8sgpt/issues/806)) ([d6fb648](https://github.com/k8sgpt-ai/k8sgpt/commit/d6fb648e23c1ed1e4680fc4b7b4e96501f50ad48))
|
||||
* **deps:** update anchore/sbom-action action to v0.15.1 ([#784](https://github.com/k8sgpt-ai/k8sgpt/issues/784)) ([6473a2b](https://github.com/k8sgpt-ai/k8sgpt/commit/6473a2b532491b707b3af922fc2198e626ebf219))
|
||||
* **deps:** update google-github-actions/release-please-action action to v4 ([#782](https://github.com/k8sgpt-ai/k8sgpt/issues/782)) ([2c28c55](https://github.com/k8sgpt-ai/k8sgpt/commit/2c28c555cf4e891b90ebd9e9eae1cd8724e9886f))
|
||||
* **deps:** update google-github-actions/release-please-action action to v4.0.2 ([#800](https://github.com/k8sgpt-ai/k8sgpt/issues/800)) ([be4b0bb](https://github.com/k8sgpt-ai/k8sgpt/commit/be4b0bb3c24e04d35f40d16fd8e94ddbc8457ca6))
|
||||
|
||||
|
||||
### Refactoring
|
||||
|
||||
* replace rest client with controller-runtime clientset for Trivy analyzers ([#776](https://github.com/k8sgpt-ai/k8sgpt/issues/776)) ([1d19628](https://github.com/k8sgpt-ai/k8sgpt/commit/1d196286b75f0ea6c068e8bdb01455fb36c52432))
|
||||
|
||||
## [0.3.23](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.22...v0.3.23) (2023-11-24)
|
||||
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -98,7 +98,7 @@ vet:
|
||||
## lint: Run go lint against code.
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@golangci-lint run -v ./...
|
||||
@golangci-lint run -v --timeout=5m ./...
|
||||
|
||||
## style: Code style -> fmt,vet,lint
|
||||
.PHONY: style
|
||||
|
||||
465
README.md
465
README.md
@@ -9,23 +9,34 @@
|
||||

|
||||
[](https://bestpractices.coreinfrastructure.org/projects/7272)
|
||||
[](https://docs.k8sgpt.ai/)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fk8sgpt-ai%2Fk8sgpt?ref=badge_shield)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://github.com/k8sgpt-ai/k8sgpt)
|
||||
[](https://codecov.io/github/k8sgpt-ai/k8sgpt)
|
||||

|
||||
|
||||
`k8sgpt` is a tool for scanning your Kubernetes clusters, diagnosing, and triaging issues in simple English.
|
||||
|
||||
It has SRE experience codified into its analyzers and helps to pull out the most relevant information to enrich it with AI.
|
||||
|
||||
_Out of the box integration with OpenAI, Azure, Cohere, Amazon Bedrock and local models._
|
||||
_Out of the box integration with OpenAI, Azure, Cohere, Amazon Bedrock, Google Gemini and local models._
|
||||
|
||||
<a href="https://www.producthunt.com/posts/k8sgpt?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-k8sgpt" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=389489&theme=light" alt="K8sGPT - K8sGPT gives Kubernetes Superpowers to everyone | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://hellogithub.com/repository/9dfe44c18dfb4d6fa0181baf8b2cf2e1" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=9dfe44c18dfb4d6fa0181baf8b2cf2e1&claim_uid=gqG4wmzkMrP0eFy" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<a href="https://www.producthunt.com/posts/k8sgpt?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-k8sgpt" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=389489&theme=light" alt="K8sGPT - K8sGPT gives Kubernetes Superpowers to everyone | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<img src="images/demo4.gif" width=650px; />
|
||||
|
||||
# CLI Installation
|
||||
|
||||
|
||||
### Linux/Mac via brew
|
||||
|
||||
```sh
|
||||
$ brew install k8sgpt
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
brew tap k8sgpt-ai/k8sgpt
|
||||
brew install k8sgpt
|
||||
```
|
||||
@@ -33,20 +44,20 @@ brew install k8sgpt
|
||||
<details>
|
||||
<summary>RPM-based installation (RedHat/CentOS/Fedora)</summary>
|
||||
|
||||
**32 bit:**
|
||||
**32 bit:**
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.23/k8sgpt_386.rpm
|
||||
sudo rpm -ivh k8sgpt_386.rpm
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.48/k8sgpt_386.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
|
||||
**64 bit:**
|
||||
**64 bit:**
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.23/k8sgpt_amd64.rpm
|
||||
sudo rpm -ivh -i k8sgpt_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.48/k8sgpt_amd64.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
</details>
|
||||
@@ -54,20 +65,26 @@ brew install k8sgpt
|
||||
<details>
|
||||
<summary>DEB-based installation (Ubuntu/Debian)</summary>
|
||||
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.23/k8sgpt_386.deb
|
||||
sudo dpkg -i k8sgpt_386.deb
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
**64 bit:**
|
||||
**32 bit:**
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.23/k8sgpt_amd64.deb
|
||||
sudo dpkg -i k8sgpt_amd64.deb
|
||||
```
|
||||
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.48/k8sgpt_386.deb
|
||||
sudo dpkg -i k8sgpt_386.deb
|
||||
```
|
||||
|
||||
<!---x-release-please-end-->
|
||||
|
||||
**64 bit:**
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.48/k8sgpt_amd64.deb
|
||||
sudo dpkg -i k8sgpt_amd64.deb
|
||||
```
|
||||
|
||||
<!---x-release-please-end-->
|
||||
</details>
|
||||
|
||||
@@ -75,44 +92,48 @@ brew install k8sgpt
|
||||
|
||||
<summary>APK-based installation (Alpine)</summary>
|
||||
|
||||
**32 bit:**
|
||||
**32 bit:**
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.23/k8sgpt_386.apk
|
||||
apk add k8sgpt_386.apk
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.48/k8sgpt_386.apk
|
||||
apk add --allow-untrusted k8sgpt_386.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
**64 bit:**
|
||||
|
||||
**64 bit:**
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.23/k8sgpt_amd64.apk
|
||||
apk add k8sgpt_amd64.apk
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.48/k8sgpt_amd64.apk
|
||||
apk add --allow-untrusted k8sgpt_amd64.apk
|
||||
```
|
||||
<!---x-release-please-end-->x
|
||||
<!---x-release-please-end-->
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Failing Installation on WSL or Linux (missing gcc)</summary>
|
||||
When installing Homebrew on WSL or Linux, you may encounter the following error:
|
||||
|
||||
```
|
||||
==> Installing k8sgpt from k8sgpt-ai/k8sgpt Error: The following formula cannot be installed from a bottle and must be
|
||||
built from the source. k8sgpt Install Clang or run brew install gcc.
|
||||
```
|
||||
```
|
||||
==> Installing k8sgpt from k8sgpt-ai/k8sgpt Error: The following formula cannot be installed from a bottle and must be
|
||||
built from the source. k8sgpt Install Clang or run brew install gcc.
|
||||
```
|
||||
|
||||
If you install gcc as suggested, the problem will persist. Therefore, you need to install the build-essential package.
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install build-essential
|
||||
```
|
||||
</details>
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install build-essential
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Windows
|
||||
|
||||
* Download the latest Windows binaries of **k8sgpt** from the [Release](https://github.com/k8sgpt-ai/k8sgpt/releases)
|
||||
- Download the latest Windows binaries of **k8sgpt** from the [Release](https://github.com/k8sgpt-ai/k8sgpt/releases)
|
||||
tab based on your system architecture.
|
||||
* Extract the downloaded package to your desired location. Configure the system *path* variable with the binary location
|
||||
- Extract the downloaded package to your desired location. Configure the system _path_ variable with the binary location
|
||||
|
||||
## Operator Installation
|
||||
|
||||
@@ -120,17 +141,16 @@ To install within a Kubernetes cluster please use our `k8sgpt-operator` with ins
|
||||
|
||||
_This mode of operation is ideal for continuous monitoring of your cluster and can integrate with your existing monitoring such as Prometheus and Alertmanager._
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
* Currently the default AI provider is OpenAI, you will need to generate an API key from [OpenAI](https://openai.com)
|
||||
* You can do this by running `k8sgpt generate` to open a browser link to generate it
|
||||
* Run `k8sgpt auth add` to set it in k8sgpt.
|
||||
* You can provide the password directly using the `--password` flag.
|
||||
* Run `k8sgpt filters` to manage the active filters used by the analyzer. By default, all filters are executed during analysis.
|
||||
* Run `k8sgpt analyze` to run a scan.
|
||||
* And use `k8sgpt analyze --explain` to get a more detailed explanation of the issues.
|
||||
* You also run `k8sgpt analyze --with-doc` (with or without the explain flag) to get the official documentation from kubernetes.
|
||||
- Currently, the default AI provider is OpenAI, you will need to generate an API key from [OpenAI](https://openai.com)
|
||||
- You can do this by running `k8sgpt generate` to open a browser link to generate it
|
||||
- Run `k8sgpt auth add` to set it in k8sgpt.
|
||||
- You can provide the password directly using the `--password` flag.
|
||||
- Run `k8sgpt filters` to manage the active filters used by the analyzer. By default, all filters are executed during analysis.
|
||||
- Run `k8sgpt analyze` to run a scan.
|
||||
- And use `k8sgpt analyze --explain` to get a more detailed explanation of the issues.
|
||||
- You also run `k8sgpt analyze --with-doc` (with or without the explain flag) to get the official documentation from Kubernetes.
|
||||
|
||||
## Analyzers
|
||||
|
||||
@@ -162,6 +182,7 @@ you will be able to write your own analyzers.
|
||||
- [x] gatewayClass
|
||||
- [x] gateway
|
||||
- [x] httproute
|
||||
- [x] logAnalyzer
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -181,6 +202,7 @@ k8sgpt analyze --explain --filter=Service
|
||||
```
|
||||
|
||||
_Filter by namespace_
|
||||
|
||||
```
|
||||
k8sgpt analyze --explain --filter=Pod --namespace=default
|
||||
```
|
||||
@@ -285,204 +307,69 @@ k8sgpt serve
|
||||
_Analysis with serve mode_
|
||||
|
||||
```
|
||||
curl -X GET "http://localhost:8080/analyze?namespace=k8sgpt&explain=false"
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
## Key Features
|
||||
|
||||
<details>
|
||||
<summary> LocalAI provider </summary>
|
||||
|
||||
To run local models, it is possible to use OpenAI compatible APIs, for instance [LocalAI](https://github.com/go-skynet/LocalAI) which uses [llama.cpp](https://github.com/ggerganov/llama.cpp) and [ggml](https://github.com/ggerganov/ggml) to run inference on consumer-grade hardware. Models supported by LocalAI for instance are Vicuna, Alpaca, LLaMA, Cerebras, GPT4ALL, GPT4ALL-J and koala.
|
||||
|
||||
|
||||
To run local inference, you need to download the models first, for instance you can find `ggml` compatible models in [huggingface.com](https://huggingface.co/models?search=ggml) (for example vicuna, alpaca and koala).
|
||||
|
||||
### Start the API server
|
||||
|
||||
To start the API server, follow the instruction in [LocalAI](https://github.com/go-skynet/LocalAI#example-use-gpt4all-j-model).
|
||||
|
||||
### Run k8sgpt
|
||||
|
||||
To run k8sgpt, run `k8sgpt auth add` with the `localai` backend:
|
||||
|
||||
```
|
||||
k8sgpt auth add --backend localai --model <model_name> --baseurl http://localhost:8080/v1 --temperature 0.7
|
||||
grpcurl -plaintext -d '{"namespace": "k8sgpt", "explain" : "true"}' localhost:8080 schema.v1.ServerAnalyzerService/Analyze
|
||||
{
|
||||
"status": "OK"
|
||||
}
|
||||
```
|
||||
|
||||
Now you can analyze with the `localai` backend:
|
||||
_Analysis with custom headers_
|
||||
|
||||
```
|
||||
k8sgpt analyze --explain --backend localai
|
||||
k8sgpt analyze --explain --custom-headers CustomHeaderKey:CustomHeaderValue
|
||||
```
|
||||
|
||||
_Print analysis stats_
|
||||
|
||||
```
|
||||
k8sgpt analyze -s
|
||||
The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.
|
||||
- Analyzer Ingress took 47.125583ms
|
||||
- Analyzer PersistentVolumeClaim took 53.009167ms
|
||||
- Analyzer CronJob took 57.517792ms
|
||||
- Analyzer Deployment took 156.6205ms
|
||||
- Analyzer Node took 160.109833ms
|
||||
- Analyzer ReplicaSet took 245.938333ms
|
||||
- Analyzer StatefulSet took 448.0455ms
|
||||
- Analyzer Pod took 5.662594708s
|
||||
- Analyzer Service took 38.583359166s
|
||||
```
|
||||
|
||||
_Diagnostic information_
|
||||
|
||||
To collect diagnostic information use the following command to create a `dump_<timestamp>_json` in your local directory.
|
||||
```
|
||||
k8sgpt dump
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> AzureOpenAI provider </summary>
|
||||
## LLM AI Backends
|
||||
|
||||
<em>Prerequisites:</em> an Azure OpenAI deployment is needed, please visit MS official [documentation](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource) to create your own.
|
||||
K8sGPT uses the chosen LLM, generative AI provider when you want to explain the analysis results using --explain flag e.g. `k8sgpt analyze --explain`. You can use `--backend` flag to specify a configured provider (it's `openai` by default).
|
||||
|
||||
To authenticate with k8sgpt, you will need the Azure OpenAI endpoint of your tenant `"https://your Azure OpenAI Endpoint"`, the api key to access your deployment, the deployment name of your model and the model name itself.
|
||||
|
||||
|
||||
To run k8sgpt, run `k8sgpt auth` with the `azureopenai` backend:
|
||||
```
|
||||
k8sgpt auth add --backend azureopenai --baseurl https://<your Azure OpenAI endpoint> --engine <deployment_name> --model <model_name>
|
||||
```
|
||||
Lastly, enter your Azure API key, after the prompt.
|
||||
|
||||
Now you are ready to analyze with the azure openai backend:
|
||||
```
|
||||
k8sgpt analyze --explain --backend azureopenai
|
||||
```
|
||||
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Cohere provider</summary>
|
||||
|
||||
<em>Prerequisites:</em> a Cohere API key is needed, please visit the [Cohere dashboard](https://dashboard.cohere.ai/api-keys) to create one.
|
||||
|
||||
To run k8sgpt, run `k8sgpt auth` with the `cohere` backend:
|
||||
You can list available providers using `k8sgpt auth list`:
|
||||
|
||||
```
|
||||
k8sgpt auth add --backend cohere --model command-nightly
|
||||
```
|
||||
|
||||
Lastly, enter your Cohere API key, after the prompt.
|
||||
|
||||
Now you are ready to analyze with the Cohere backend:
|
||||
|
||||
```
|
||||
k8sgpt analyze --explain --backend cohere
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Amazon Bedrock provider</summary>
|
||||
|
||||
|
||||
<em>Prerequisites</em>
|
||||
Bedrock API access is needed.
|
||||
|
||||
<img src="images/bedrock.png" width="500px;" />
|
||||
|
||||
As illustrated below, you will need to enable this in the [AWS Console](https://eu-central-1.console.aws.amazon.com/bedrock/home?region=eu-central-1#/modelaccess)
|
||||
|
||||
In addition to this you will need to set the follow local environmental variables:
|
||||
|
||||
|
||||
```
|
||||
- AWS_ACCESS_KEY
|
||||
- AWS_SECRET_ACCESS_KEY
|
||||
- AWS_DEFAULT_REGION
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
k8sgpt auth add --backend amazonbedrock --model anthropic.claude-v2
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
```
|
||||
k8sgpt analyze -e -b amazonbedrock
|
||||
|
||||
0 argocd/argocd-application-controller(argocd-application-controller)
|
||||
- Error: StatefulSet uses the service argocd/argocd-application-controller which does not exist.
|
||||
|
||||
You're right, I don't have enough context to determine if a StatefulSet is correctly configured to use a non-existent service. A StatefulSet manages Pods with persistent storage, and the Pods are created from the same spec. The service name referenced in the StatefulSet configuration would need to match an existing Kubernetes service for the Pods to connect to. Without more details on the specific StatefulSet and environment, I can't confirm whether the configuration is valid or not.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Amazon SageMaker Provider</summary>
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
1. **AWS CLI Configuration**: Make sure you have the AWS Command Line Interface (CLI) configured on your machine. If you haven't already configured the AWS CLI, you can follow the official AWS documentation for instructions on how to do it: [AWS CLI Configuration Guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html).
|
||||
|
||||
2. **SageMaker Instance**: You need to have an Amazon SageMaker instance set up. If you don't have one already, you can follow the step-by-step instructions provided in this repository for creating a SageMaker instance: [llm-sagemaker-jumpstart-cdk](https://github.com/zaremb/llm-sagemaker-jumpstart-cdk).
|
||||
|
||||
#### Backend Configuration
|
||||
|
||||
To add amazonsagemaker backend two parameters are required:
|
||||
|
||||
* `--endpointname` Amazon SageMaker endpoint name.
|
||||
* `--providerRegion` AWS region where SageMaker instance is created. `k8sgpt` uses this region to connect to SageMaker (not the one defined with AWS CLI or environment variables )
|
||||
|
||||
To add amazonsagemaker as a backend run:
|
||||
|
||||
```bash
|
||||
k8sgpt auth add --backend amazonsagemaker --providerRegion eu-west-1 --endpointname endpoint-xxxxxxxxxx
|
||||
```
|
||||
|
||||
#### Optional params
|
||||
|
||||
Optionally, when adding the backend and later by changing the configuration file, you can set the following parameters:
|
||||
|
||||
`-l, --maxtokens int` Specify a maximum output length. Adjust (1-...) to control text length. Higher values produce longer output, lower values limit length (default 2048)
|
||||
|
||||
`-t, --temperature float32` The sampling temperature, value ranges between 0 ( output be more deterministic) and 1 (more random) (default 0.7)
|
||||
|
||||
`-c, --topp float32` Probability Cutoff: Set a threshold (0.0-1.0) to limit word choices. Higher values add randomness, lower values increase predictability. (default 0.5)
|
||||
|
||||
To make amazonsagemaker as a default backend run:
|
||||
|
||||
```bash
|
||||
k8sgpt auth default -p amazonsagemaker
|
||||
```
|
||||
|
||||
#### AmazonSageMaker Usage
|
||||
|
||||
```bash
|
||||
./k8sgpt analyze -e -b amazonsagemaker
|
||||
100% |███████████████████████████████████████████████████████████████████████████████████████████████████████████████████| (1/1, 14 it/min)
|
||||
AI Provider: amazonsagemaker
|
||||
|
||||
0 default/nginx(nginx)
|
||||
- Error: Back-off pulling image "nginxx"
|
||||
Error: Back-off pulling image "nginxx"
|
||||
|
||||
Solution:
|
||||
|
||||
1. Check if the image exists in the registry by running `docker image ls nginxx`.
|
||||
2. If the image is not found, try pulling it by running `docker pull nginxx`.
|
||||
3. If the image is still not available, check if there are any network issues by running `docker network inspect` and `docker network list`.
|
||||
4. If the issue persists, try restarting the Docker daemon by running `sudo service docker restart`.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Setting a new default AI provider</summary>
|
||||
|
||||
There may be scenarios where you wish to have K8sGPT plugged into several default AI providers. In this case you may wish to use one as a new default, other than OpenAI which is the project default.
|
||||
|
||||
_To view available providers_
|
||||
|
||||
```
|
||||
k8sgpt auth list
|
||||
Default:
|
||||
> openai
|
||||
Active:
|
||||
> openai
|
||||
> azureopenai
|
||||
Unused:
|
||||
> openai
|
||||
> localai
|
||||
> noopai
|
||||
> amazonbedrock
|
||||
> ollama
|
||||
> azureopenai
|
||||
> cohere
|
||||
|
||||
> amazonbedrock
|
||||
> amazonsagemaker
|
||||
> google
|
||||
> huggingface
|
||||
> noopai
|
||||
> googlevertexai
|
||||
> ibmwatsonxai
|
||||
```
|
||||
|
||||
For detailed documentation on how to configure and use each provider see [here](https://docs.k8sgpt.ai/reference/providers/backend/).
|
||||
|
||||
_To set a new default provider_
|
||||
|
||||
@@ -491,33 +378,34 @@ k8sgpt auth default -p azureopenai
|
||||
Default provider set to azureopenai
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
## Key Features
|
||||
|
||||
<details>
|
||||
|
||||
With this option, the data is anonymized before being sent to the AI Backend. During the analysis execution, `k8sgpt` retrieves sensitive data (Kubernetes object names, labels, etc.). This data is masked when sent to the AI backend and replaced by a key that can be used to de-anonymize the data when the solution is returned to the user.
|
||||
|
||||
|
||||
<summary> Anonymization </summary>
|
||||
|
||||
1. Error reported during analysis:
|
||||
|
||||
```bash
|
||||
Error: HorizontalPodAutoscaler uses StatefulSet/fake-deployment as ScaleTargetRef which does not exist.
|
||||
```
|
||||
|
||||
2. Payload sent to the AI backend:
|
||||
|
||||
```bash
|
||||
Error: HorizontalPodAutoscaler uses StatefulSet/tGLcCRcHa1Ce5Rs as ScaleTargetRef which does not exist.
|
||||
```
|
||||
|
||||
3. Payload returned by the AI:
|
||||
|
||||
```bash
|
||||
The Kubernetes system is trying to scale a StatefulSet named tGLcCRcHa1Ce5Rs using the HorizontalPodAutoscaler, but it cannot find the StatefulSet. The solution is to verify that the StatefulSet name is spelled correctly and exists in the same namespace as the HorizontalPodAutoscaler.
|
||||
```
|
||||
|
||||
4. Payload returned to the user:
|
||||
|
||||
```bash
|
||||
The Kubernetes system is trying to scale a StatefulSet named fake-deployment using the HorizontalPodAutoscaler, but it cannot find the StatefulSet. The solution is to verify that the StatefulSet name is spelled correctly and exists in the same namespace as the HorizontalPodAutoscaler.
|
||||
```
|
||||
@@ -528,7 +416,7 @@ Note: **Anonymization does not currently apply to events.**
|
||||
|
||||
**Anonymization does not currently apply to events.**
|
||||
|
||||
*In a few analysers like Pod, we feed to the AI backend the event messages which are not known beforehand thus we are not masking them for the **time being**.*
|
||||
_In a few analysers like Pod, we feed to the AI backend the event messages which are not known beforehand thus we are not masking them for the **time being**._
|
||||
|
||||
- The following is the list of analysers in which data is **being masked**:-
|
||||
|
||||
@@ -547,11 +435,13 @@ Note: **Anonymization does not currently apply to events.**
|
||||
- RepicaSet
|
||||
- PersistentVolumeClaim
|
||||
- Pod
|
||||
- **_*Events_**
|
||||
- Log
|
||||
- **_\*Events_**
|
||||
|
||||
***Note**:
|
||||
- k8gpt will not mask the above analysers because they do not send any identifying information except **Events** analyser.
|
||||
- Masking for **Events** analyzer is scheduled in the near future as seen in this [issue](https://github.com/k8sgpt-ai/k8sgpt/issues/560). _Further research has to be made to understand the patterns and be able to mask the sensitive parts of an event like pod name, namespace etc._
|
||||
**\*Note**:
|
||||
|
||||
- k8gpt will not mask the above analysers because they do not send any identifying information except **Events** analyser.
|
||||
- Masking for **Events** analyzer is scheduled in the near future as seen in this [issue](https://github.com/k8sgpt-ai/k8sgpt/issues/560). _Further research has to be made to understand the patterns and be able to mask the sensitive parts of an event like pod name, namespace etc._
|
||||
|
||||
- The following is the list of fields which are not **being masked**:-
|
||||
|
||||
@@ -559,18 +449,18 @@ Note: **Anonymization does not currently apply to events.**
|
||||
- ObjectStatus
|
||||
- Replicas
|
||||
- ContainerStatus
|
||||
- **_*Event Message_**
|
||||
- **_\*Event Message_**
|
||||
- ReplicaStatus
|
||||
- Count (Pod)
|
||||
|
||||
***Note**:
|
||||
- It is quite possible the payload of the event message might have something like "super-secret-project-pod-X crashed" which we don't currently redact _(scheduled in the near future as seen in this [issue](https://github.com/k8sgpt-ai/k8sgpt/issues/560))_.
|
||||
**\*Note**:
|
||||
|
||||
- It is quite possible the payload of the event message might have something like "super-secret-project-pod-X crashed" which we don't currently redact _(scheduled in the near future as seen in this [issue](https://github.com/k8sgpt-ai/k8sgpt/issues/560))_.
|
||||
|
||||
### Proceed with care
|
||||
|
||||
- The K8gpt team recommends using an entirely different backend **(a local model) in critical production environments**. By using a local model, you can rest assured that everything stays within your DMZ, and nothing is leaked.
|
||||
- If there is any uncertainty about the possibility of sending data to a public LLM (open AI, Azure AI) and it poses a risk to business-critical operations, then, in such cases, the use of public LLM should be avoided based on personal assessment and the jurisdiction of risks involved.
|
||||
|
||||
- The K8gpt team recommends using an entirely different backend **(a local model) in critical production environments**. By using a local model, you can rest assured that everything stays within your DMZ, and nothing is leaked.
|
||||
- If there is any uncertainty about the possibility of sending data to a public LLM (open AI, Azure AI) and it poses a risk to business-critical operations, then, in such cases, the use of public LLM should be avoided based on personal assessment and the jurisdiction of risks involved.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -580,66 +470,121 @@ Note: **Anonymization does not currently apply to events.**
|
||||
`k8sgpt` stores config data in the `$XDG_CONFIG_HOME/k8sgpt/k8sgpt.yaml` file. The data is stored in plain text, including your OpenAI key.
|
||||
|
||||
Config file locations:
|
||||
| OS | Path |
|
||||
| OS | Path |
|
||||
| ------- | ------------------------------------------------ |
|
||||
| MacOS | ~/Library/Application Support/k8sgpt/k8sgpt.yaml |
|
||||
| Linux | ~/.config/k8sgpt/k8sgpt.yaml |
|
||||
| Windows | %LOCALAPPDATA%/k8sgpt/k8sgpt.yaml |
|
||||
| MacOS | ~/Library/Application Support/k8sgpt/k8sgpt.yaml |
|
||||
| Linux | ~/.config/k8sgpt/k8sgpt.yaml |
|
||||
| Windows | %LOCALAPPDATA%/k8sgpt/k8sgpt.yaml |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
There may be scenarios where caching remotely is preferred.
|
||||
In these scenarios K8sGPT supports AWS S3 or Azure Blob storage Integration.
|
||||
|
||||
<summary> Remote caching </summary>
|
||||
<summary> Remote caching </summary>
|
||||
<em>Note: You can only configure and use only one remote cache at a time</em>
|
||||
|
||||
_Adding a remote cache_
|
||||
|
||||
* AWS S3
|
||||
* _As a prerequisite `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are required as environmental variables._
|
||||
* Configuration, ``` k8sgpt cache add s3 --region <aws region> --bucket <name> ```
|
||||
* K8sGPT will create the bucket if it does not exist
|
||||
* Azure Storage
|
||||
* We support a number of [techniques](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication?tabs=bash#2-authenticate-with-azure) to authenticate against Azure
|
||||
* Configuration, ``` k8sgpt cache add azure --storageacc <storage account name> --container <container name> ```
|
||||
* K8sGPT assumes that the storage account already exist and it will create the container if it does not exist
|
||||
* It is the **user** responsibility have to grant specific permissions to their identity in order to be able to upload blob files and create SA containers (e.g Storage Blob Data Contributor)
|
||||
* Google Cloud Storage
|
||||
* _As a prerequisite `GOOGLE_APPLICATION_CREDENTIALS` are required as environmental variables._
|
||||
* Configuration, ``` k8sgpt cache add gcs --region <gcp region> --bucket <name> --projectid <project id>```
|
||||
* K8sGPT will create the bucket if it does not exist
|
||||
- AWS S3
|
||||
- _As a prerequisite `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are required as environmental variables._
|
||||
- Configuration, `k8sgpt cache add s3 --region <aws region> --bucket <name>`
|
||||
- Minio Configuration with HTTP endpoint ` k8sgpt cache add s3 --bucket <name> --endpoint <http://localhost:9000>`
|
||||
- Minio Configuration with HTTPs endpoint, skipping TLS verification ` k8sgpt cache add s3 --bucket <name> --endpoint <https://localhost:9000> --insecure`
|
||||
- K8sGPT will create the bucket if it does not exist
|
||||
- Azure Storage
|
||||
- We support a number of [techniques](https://learn.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication?tabs=bash#2-authenticate-with-azure) to authenticate against Azure
|
||||
- Configuration, `k8sgpt cache add azure --storageacc <storage account name> --container <container name>`
|
||||
- K8sGPT assumes that the storage account already exist and it will create the container if it does not exist
|
||||
- It is the **user** responsibility have to grant specific permissions to their identity in order to be able to upload blob files and create SA containers (e.g Storage Blob Data Contributor)
|
||||
- Google Cloud Storage
|
||||
- _As a prerequisite `GOOGLE_APPLICATION_CREDENTIALS` are required as environmental variables._
|
||||
- Configuration, ` k8sgpt cache add gcs --region <gcp region> --bucket <name> --projectid <project id>`
|
||||
- K8sGPT will create the bucket if it does not exist
|
||||
|
||||
_Listing cache items_
|
||||
|
||||
```
|
||||
k8sgpt cache list
|
||||
```
|
||||
|
||||
_Purging an object from the cache_
|
||||
Note: purging an object using this command will delete upstream files, so it requires appropriate permissions.
|
||||
|
||||
```
|
||||
k8sgpt cache purge $OBJECT_NAME
|
||||
```
|
||||
|
||||
_Removing the remote cache_
|
||||
Note: this will not delete the upstream S3 bucket or Azure storage container
|
||||
|
||||
```
|
||||
k8sgpt cache remove
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> Custom Analyzers</summary>
|
||||
|
||||
There may be scenarios where you wish to write your own analyzer in a language of your choice.
|
||||
K8sGPT now supports the ability to do so by abiding by the [schema](https://github.com/k8sgpt-ai/schemas/blob/main/protobuf/schema/v1/analyzer.proto) and serving the analyzer for consumption.
|
||||
To do so, define the analyzer within the K8sGPT configuration and it will add it into the scanning process.
|
||||
In addition to this you will need to enable the following flag on analysis:
|
||||
|
||||
```
|
||||
k8sgpt analyze --custom-analysis
|
||||
```
|
||||
|
||||
Here is an example local host analyzer in [Rust](https://github.com/k8sgpt-ai/host-analyzer)
|
||||
When this is run on `localhost:8080` the K8sGPT config can pick it up with the following additions:
|
||||
|
||||
```
|
||||
custom_analyzers:
|
||||
- name: host-analyzer
|
||||
connection:
|
||||
url: localhost
|
||||
port: 8080
|
||||
```
|
||||
|
||||
This now gives the ability to pass through hostOS information ( from this analyzer example ) to K8sGPT to use as context with normal analysis.
|
||||
|
||||
_See the docs on how to write a custom analyzer_
|
||||
|
||||
_Listing custom analyzers configured_
|
||||
```
|
||||
k8sgpt custom-analyzer list
|
||||
```
|
||||
|
||||
_Adding custom analyzer without install_
|
||||
```
|
||||
k8sgpt custom-analyzer add --name my-custom-analyzer --port 8085
|
||||
```
|
||||
|
||||
_Removing custom analyzer_
|
||||
```
|
||||
k8sgpt custom-analyzer remove --names "my-custom-analyzer,my-custom-analyzer-2"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Documentation
|
||||
|
||||
Find our official documentation available [here](https://docs.k8sgpt.ai)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read our [contributing guide](./CONTRIBUTING.md).
|
||||
|
||||
## Community
|
||||
|
||||
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-276pa9uyq-pxAUr4TCVHubFxEvLZuT1Q)
|
||||
|
||||
<a href="https://github.com/k8sgpt-ai/k8sgpt/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=k8sgpt-ai/k8sgpt" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fk8sgpt-ai%2Fk8sgpt?ref=badge_large)
|
||||
|
||||
@@ -21,6 +21,10 @@ spec:
|
||||
app.kubernetes.io/name: {{ include "k8sgpt.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
{{- if .Values.deployment.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.deployment.securityContext | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ template "k8sgpt.fullname" . }}
|
||||
containers:
|
||||
- name: k8sgpt-container
|
||||
|
||||
@@ -14,7 +14,10 @@ deployment:
|
||||
requests:
|
||||
cpu: "0.2"
|
||||
memory: "156Mi"
|
||||
|
||||
securityContext: {}
|
||||
# Set securityContext.runAsUser/runAsGroup if necessary. Values below were taken from https://github.com/k8sgpt-ai/k8sgpt/blob/main/container/Dockerfile
|
||||
# runAsUser: 65532
|
||||
# runAsGroup: 65532
|
||||
secret:
|
||||
secretKey: "" # base64 encoded OpenAI token
|
||||
|
||||
|
||||
@@ -16,23 +16,31 @@ package analyze
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/interactive"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
explain bool
|
||||
backend string
|
||||
output string
|
||||
filters []string
|
||||
language string
|
||||
nocache bool
|
||||
namespace string
|
||||
anonymize bool
|
||||
maxConcurrency int
|
||||
withDoc bool
|
||||
explain bool
|
||||
backend string
|
||||
output string
|
||||
filters []string
|
||||
language string
|
||||
nocache bool
|
||||
namespace string
|
||||
labelSelector string
|
||||
anonymize bool
|
||||
maxConcurrency int
|
||||
withDoc bool
|
||||
interactiveMode bool
|
||||
customAnalysis bool
|
||||
customHeaders []string
|
||||
withStats bool
|
||||
)
|
||||
|
||||
// AnalyzeCmd represents the problems command
|
||||
@@ -43,37 +51,81 @@ var AnalyzeCmd = &cobra.Command{
|
||||
Long: `This command will find problems within your Kubernetes cluster and
|
||||
provide you with a list of issues that need to be resolved`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Create analysis configuration first.
|
||||
config, err := analysis.NewAnalysis(
|
||||
backend,
|
||||
language,
|
||||
filters,
|
||||
namespace,
|
||||
labelSelector,
|
||||
nocache,
|
||||
explain,
|
||||
maxConcurrency,
|
||||
withDoc,
|
||||
interactiveMode,
|
||||
customHeaders,
|
||||
withStats,
|
||||
)
|
||||
|
||||
// AnalysisResult configuration
|
||||
config, err := analysis.NewAnalysis(backend,
|
||||
language, filters, namespace, nocache, explain, maxConcurrency, withDoc)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer config.Close()
|
||||
|
||||
if customAnalysis {
|
||||
config.RunCustomAnalysis()
|
||||
}
|
||||
config.RunAnalysis()
|
||||
|
||||
if explain {
|
||||
err := config.GetAIResults(output, anonymize)
|
||||
if err != nil {
|
||||
if err := config.GetAIResults(output, anonymize); err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// print results
|
||||
output, err := config.PrintOutput(output)
|
||||
output_data, err := config.PrintOutput(output)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
|
||||
if withStats {
|
||||
statsData := config.PrintStats()
|
||||
fmt.Println(string(statsData))
|
||||
}
|
||||
|
||||
fmt.Println(string(output_data))
|
||||
|
||||
if interactiveMode && explain {
|
||||
if output == "json" {
|
||||
color.Yellow("Caution: interactive mode using --json enabled may use additional tokens.")
|
||||
}
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
interactiveClient := interactive.NewInteractionRunner(config, output_data)
|
||||
|
||||
go interactiveClient.StartInteraction()
|
||||
for {
|
||||
select {
|
||||
case res := <-sigs:
|
||||
switch res {
|
||||
default:
|
||||
os.Exit(0)
|
||||
}
|
||||
case res := <-interactiveClient.State:
|
||||
switch res {
|
||||
case interactive.E_EXITED:
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
// namespace flag
|
||||
AnalyzeCmd.Flags().StringVarP(&namespace, "namespace", "n", "", "Namespace to analyze")
|
||||
// no cache flag
|
||||
@@ -85,7 +137,7 @@ func init() {
|
||||
// explain flag
|
||||
AnalyzeCmd.Flags().BoolVarP(&explain, "explain", "e", false, "Explain the problem to me")
|
||||
// add flag for backend
|
||||
AnalyzeCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
|
||||
AnalyzeCmd.Flags().StringVarP(&backend, "backend", "b", "", "Backend AI provider")
|
||||
// output as json
|
||||
AnalyzeCmd.Flags().StringVarP(&output, "output", "o", "text", "Output format (text, json)")
|
||||
// add language options for output
|
||||
@@ -94,4 +146,14 @@ func init() {
|
||||
AnalyzeCmd.Flags().IntVarP(&maxConcurrency, "max-concurrency", "m", 10, "Maximum number of concurrent requests to the Kubernetes API server")
|
||||
// kubernetes doc flag
|
||||
AnalyzeCmd.Flags().BoolVarP(&withDoc, "with-doc", "d", false, "Give me the official documentation of the involved field")
|
||||
// interactive mode flag
|
||||
AnalyzeCmd.Flags().BoolVarP(&interactiveMode, "interactive", "i", false, "Enable interactive mode that allows further conversation with LLM about the problem. Works only with --explain flag")
|
||||
// custom analysis flag
|
||||
AnalyzeCmd.Flags().BoolVarP(&customAnalysis, "custom-analysis", "z", false, "Enable custom analyzers")
|
||||
// add custom headers flag
|
||||
AnalyzeCmd.Flags().StringSliceVarP(&customHeaders, "custom-headers", "r", []string{}, "Custom Headers, <key>:<value> (e.g CustomHeaderKey:CustomHeaderValue AnotherHeader:AnotherValue)")
|
||||
// label selector flag
|
||||
AnalyzeCmd.Flags().StringVarP(&labelSelector, "selector", "L", "", "Label selector (label query) to filter on, supports '=', '==', and '!='. (e.g. -L key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.")
|
||||
// print stats
|
||||
AnalyzeCmd.Flags().BoolVarP(&withStats, "with-stat", "s", false, "Print analysis stats. This option disables errors display.")
|
||||
}
|
||||
|
||||
@@ -45,25 +45,15 @@ var addCmd = &cobra.Command{
|
||||
_ = cmd.MarkFlagRequired("endpointname")
|
||||
_ = cmd.MarkFlagRequired("providerRegion")
|
||||
}
|
||||
if strings.ToLower(backend) == "amazonbedrock" {
|
||||
_ = cmd.MarkFlagRequired("providerRegion")
|
||||
}
|
||||
if strings.ToLower(backend) == "ibmwatsonxai" {
|
||||
_ = cmd.MarkFlagRequired("providerId")
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// get ai configuration
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// search for provider with same name
|
||||
providerIndex := -1
|
||||
for i, provider := range configAI.Providers {
|
||||
if backend == provider.Name {
|
||||
providerIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
validBackend := func(validBackends []string, backend string) bool {
|
||||
for _, b := range validBackends {
|
||||
if b == backend {
|
||||
@@ -84,6 +74,28 @@ var addCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
// get ai configuration
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// search for provider with same name
|
||||
providerIndex := -1
|
||||
for i, provider := range configAI.Providers {
|
||||
if backend == provider.Name {
|
||||
providerIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if providerIndex != -1 {
|
||||
// provider with same name exists, update provider info
|
||||
color.Yellow("Provider with same name already exists.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// check if model is not empty
|
||||
if model == "" {
|
||||
model = defaultModel
|
||||
@@ -97,6 +109,10 @@ var addCmd = &cobra.Command{
|
||||
color.Red("Error: topP ranges from 0 to 1.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if topK < 1 || topK > 100 {
|
||||
color.Red("Error: topK ranges from 1 to 100.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if ai.NeedPassword(backend) && password == "" {
|
||||
fmt.Printf("Enter %s Key: ", backend)
|
||||
@@ -119,8 +135,12 @@ var addCmd = &cobra.Command{
|
||||
Engine: engine,
|
||||
Temperature: temperature,
|
||||
ProviderRegion: providerRegion,
|
||||
ProviderId: providerId,
|
||||
CompartmentId: compartmentId,
|
||||
TopP: topP,
|
||||
TopK: topK,
|
||||
MaxTokens: maxTokens,
|
||||
OrganizationId: organizationId,
|
||||
}
|
||||
|
||||
if providerIndex == -1 {
|
||||
@@ -132,9 +152,6 @@ var addCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
color.Green("%s added to the AI backend provider list", backend)
|
||||
} else {
|
||||
// provider with same name exists, update provider info
|
||||
color.Yellow("Provider with same name already exists.")
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -149,15 +166,23 @@ func init() {
|
||||
// add flag for url
|
||||
addCmd.Flags().StringVarP(&baseURL, "baseurl", "u", "", "URL AI provider, (e.g `http://localhost:8080/v1`)")
|
||||
// add flag for endpointName
|
||||
addCmd.Flags().StringVarP(&endpointName, "endpointname", "n", "", "Endpoint Name, (e.g `endpoint-xxxxxxxxxxxx`)")
|
||||
addCmd.Flags().StringVarP(&endpointName, "endpointname", "n", "", "Endpoint Name, e.g. `endpoint-xxxxxxxxxxxx` (only for amazonbedrock, amazonsagemaker backends)")
|
||||
// add flag for topP
|
||||
addCmd.Flags().Float32VarP(&topP, "topp", "c", 0.5, "Probability Cutoff: Set a threshold (0.0-1.0) to limit word choices. Higher values add randomness, lower values increase predictability.")
|
||||
addCmd.Flags().Float32VarP(&topP, "topp", "", 0.5, "Probability Cutoff: Set a threshold (0.0-1.0) to limit word choices. Higher values add randomness, lower values increase predictability.")
|
||||
// add flag for topK
|
||||
addCmd.Flags().Int32VarP(&topK, "topk", "c", 50, "Sampling Cutoff: Set a threshold (1-100) to restrict the sampling process to the top K most probable words at each step. Higher values lead to greater variability, lower values increases predictability.")
|
||||
// max tokens
|
||||
addCmd.Flags().IntVarP(&maxTokens, "maxtokens", "l", 2048, "Specify a maximum output length. Adjust (1-...) to control text length. Higher values produce longer output, lower values limit length")
|
||||
// add flag for temperature
|
||||
addCmd.Flags().Float32VarP(&temperature, "temperature", "t", 0.7, "The sampling temperature, value ranges between 0 ( output be more deterministic) and 1 (more random)")
|
||||
// add flag for azure open ai engine/deployment name
|
||||
addCmd.Flags().StringVarP(&engine, "engine", "e", "", "Azure AI deployment name")
|
||||
addCmd.Flags().StringVarP(&engine, "engine", "e", "", "Azure AI deployment name (only for azureopenai backend)")
|
||||
//add flag for amazonbedrock region name
|
||||
addCmd.Flags().StringVarP(&providerRegion, "providerRegion", "r", "", "Provider Region name")
|
||||
addCmd.Flags().StringVarP(&providerRegion, "providerRegion", "r", "", "Provider Region name (only for amazonbedrock, googlevertexai backend)")
|
||||
//add flag for vertexAI/WatsonxAI Project ID
|
||||
addCmd.Flags().StringVarP(&providerId, "providerId", "i", "", "Provider specific ID for e.g. project (only for googlevertexai/ibmwatsonxai backend)")
|
||||
//add flag for OCI Compartment ID
|
||||
addCmd.Flags().StringVarP(&compartmentId, "compartmentId", "k", "", "Compartment ID for generative AI model (only for oci backend)")
|
||||
// add flag for openai organization
|
||||
addCmd.Flags().StringVarP(&organizationId, "organizationId", "o", "", "OpenAI or AzureOpenAI Organization ID (only for openai and azureopenai backend)")
|
||||
}
|
||||
|
||||
@@ -27,8 +27,12 @@ var (
|
||||
engine string
|
||||
temperature float32
|
||||
providerRegion string
|
||||
providerId string
|
||||
compartmentId string
|
||||
topP float32
|
||||
topK int32
|
||||
maxTokens int
|
||||
organizationId string
|
||||
)
|
||||
|
||||
var configAI ai.AIConfiguration
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
)
|
||||
|
||||
var details bool
|
||||
var userInput string
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
@@ -39,11 +38,6 @@ var listCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if details {
|
||||
fmt.Println("Show password ? (y/n)")
|
||||
fmt.Scan(&userInput)
|
||||
}
|
||||
|
||||
// Print the default if it is set
|
||||
fmt.Print(color.YellowString("Default: \n"))
|
||||
if configAI.DefaultProvider != "" {
|
||||
@@ -66,7 +60,7 @@ var listCmd = &cobra.Command{
|
||||
if details {
|
||||
for _, provider := range configAI.Providers {
|
||||
if provider.Name == aiBackend {
|
||||
printDetails(provider, userInput)
|
||||
printDetails(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +85,7 @@ func init() {
|
||||
listCmd.Flags().BoolVar(&details, "details", false, "Print active provider configuration details")
|
||||
}
|
||||
|
||||
func printDetails(provider ai.AIProvider, userInput string) {
|
||||
func printDetails(provider ai.AIProvider) {
|
||||
if provider.Model != "" {
|
||||
fmt.Printf(" - Model: %s\n", provider.Model)
|
||||
}
|
||||
|
||||
@@ -26,13 +26,21 @@ var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Update a backend provider",
|
||||
Long: "The command to update an AI backend provider",
|
||||
Args: cobra.ExactArgs(1),
|
||||
// Args: cobra.ExactArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
_ = cmd.MarkFlagRequired("backend")
|
||||
backend, _ := cmd.Flags().GetString("backend")
|
||||
if strings.ToLower(backend) == "azureopenai" {
|
||||
_ = cmd.MarkFlagRequired("engine")
|
||||
_ = cmd.MarkFlagRequired("baseurl")
|
||||
}
|
||||
organizationId, _ := cmd.Flags().GetString("organizationId")
|
||||
if strings.ToLower(backend) != "azureopenai" && strings.ToLower(backend) != "openai" {
|
||||
if organizationId != "" {
|
||||
color.Red("Error: organizationId must be empty for backends other than azureopenai or openai.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
@@ -43,50 +51,47 @@ var updateCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
inputBackends := strings.Split(args[0], ",")
|
||||
backend, _ := cmd.Flags().GetString("backend")
|
||||
|
||||
if len(inputBackends) == 0 {
|
||||
color.Red("Error: backend must be set.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if temperature > 1.0 || temperature < 0.0 {
|
||||
color.Red("Error: temperature ranges from 0 to 1.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, b := range inputBackends {
|
||||
foundBackend := false
|
||||
for i, provider := range configAI.Providers {
|
||||
if b == provider.Name {
|
||||
foundBackend = true
|
||||
if backend != "" {
|
||||
configAI.Providers[i].Name = backend
|
||||
color.Blue("Backend name updated successfully")
|
||||
}
|
||||
if model != "" {
|
||||
configAI.Providers[i].Model = model
|
||||
color.Blue("Model updated successfully")
|
||||
}
|
||||
if password != "" {
|
||||
configAI.Providers[i].Password = password
|
||||
color.Blue("Password updated successfully")
|
||||
}
|
||||
if baseURL != "" {
|
||||
configAI.Providers[i].BaseURL = baseURL
|
||||
color.Blue("Base URL updated successfully")
|
||||
}
|
||||
if engine != "" {
|
||||
configAI.Providers[i].Engine = engine
|
||||
}
|
||||
configAI.Providers[i].Temperature = temperature
|
||||
color.Green("%s updated in the AI backend provider list", b)
|
||||
foundBackend := false
|
||||
for i, provider := range configAI.Providers {
|
||||
if backend == provider.Name {
|
||||
foundBackend = true
|
||||
if backend != "" {
|
||||
configAI.Providers[i].Name = backend
|
||||
color.Blue("Backend name updated successfully")
|
||||
}
|
||||
if model != "" {
|
||||
configAI.Providers[i].Model = model
|
||||
color.Blue("Model updated successfully")
|
||||
}
|
||||
if password != "" {
|
||||
configAI.Providers[i].Password = password
|
||||
color.Blue("Password updated successfully")
|
||||
}
|
||||
if baseURL != "" {
|
||||
configAI.Providers[i].BaseURL = baseURL
|
||||
color.Blue("Base URL updated successfully")
|
||||
}
|
||||
if engine != "" {
|
||||
configAI.Providers[i].Engine = engine
|
||||
}
|
||||
if organizationId != "" {
|
||||
configAI.Providers[i].OrganizationId = organizationId
|
||||
color.Blue("Organization Id updated successfully")
|
||||
}
|
||||
configAI.Providers[i].Temperature = temperature
|
||||
color.Green("%s updated in the AI backend provider list", backend)
|
||||
}
|
||||
if !foundBackend {
|
||||
color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
if !foundBackend {
|
||||
color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
viper.Set("ai", configAI)
|
||||
@@ -110,4 +115,6 @@ func init() {
|
||||
updateCmd.Flags().Float32VarP(&temperature, "temperature", "t", 0.7, "The sampling temperature, value ranges between 0 ( output be more deterministic) and 1 (more random)")
|
||||
// update flag for azure open ai engine/deployment name
|
||||
updateCmd.Flags().StringVarP(&engine, "engine", "e", "", "Update Azure AI deployment name")
|
||||
// update flag for organizationId
|
||||
updateCmd.Flags().StringVarP(&organizationId, "organizationId", "o", "", "Update OpenAI or Azure organization Id")
|
||||
}
|
||||
|
||||
22
cmd/cache/add.go
vendored
22
cmd/cache/add.go
vendored
@@ -17,6 +17,7 @@ package cache
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
@@ -24,11 +25,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
region string
|
||||
region string
|
||||
//nolint:unused
|
||||
bucketName string
|
||||
storageAccount string
|
||||
containerName string
|
||||
projectId string
|
||||
endpoint string
|
||||
insecure bool
|
||||
)
|
||||
|
||||
// addCmd represents the add command
|
||||
@@ -37,9 +41,10 @@ var addCmd = &cobra.Command{
|
||||
Short: "Add a remote cache",
|
||||
Long: `This command allows you to add a remote cache to store the results of an analysis.
|
||||
The supported cache types are:
|
||||
- Azure Blob storage
|
||||
- Google Cloud storage
|
||||
- S3`,
|
||||
- Azure Blob storage (e.g., k8sgpt cache add azure)
|
||||
- Google Cloud storage (e.g., k8sgpt cache add gcs)
|
||||
- S3 (e.g., k8sgpt cache add s3)
|
||||
- Interplex (e.g., k8sgpt cache add interplex)`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
color.Red("Error: Please provide a value for cache types. Run k8sgpt cache add --help")
|
||||
@@ -47,7 +52,7 @@ var addCmd = &cobra.Command{
|
||||
}
|
||||
fmt.Println(color.YellowString("Adding remote based cache"))
|
||||
cacheType := args[0]
|
||||
remoteCache, err := cache.NewCacheProvider(cacheType, bucketname, region, storageAccount, containerName, projectId)
|
||||
remoteCache, err := cache.NewCacheProvider(strings.ToLower(cacheType), bucketName, region, endpoint, storageAccount, containerName, projectId, insecure)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
@@ -62,9 +67,10 @@ var addCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
CacheCmd.AddCommand(addCmd)
|
||||
addCmd.Flags().StringVarP(®ion, "region", "r", "", "The region to use for the AWS S3 or GCS cache")
|
||||
addCmd.Flags().StringVarP(&bucketname, "bucket", "b", "", "The name of the AWS S3 bucket to use for the cache")
|
||||
addCmd.MarkFlagsRequiredTogether("region", "bucket")
|
||||
addCmd.Flags().StringVarP(®ion, "region", "r", "us-east-1", "The region to use for the AWS S3 or GCS cache")
|
||||
addCmd.Flags().StringVarP(&endpoint, "endpoint", "e", "", "The S3 or minio endpoint")
|
||||
addCmd.Flags().BoolVarP(&insecure, "insecure", "i", false, "Skip TLS verification for S3/Minio custom endpoint")
|
||||
addCmd.Flags().StringVarP(&bucketName, "bucket", "b", "", "The name of the AWS S3 bucket to use for the cache")
|
||||
addCmd.Flags().StringVarP(&projectId, "projectid", "p", "", "The GCP project ID")
|
||||
addCmd.Flags().StringVarP(&storageAccount, "storageacc", "s", "", "The Azure storage account name of the container")
|
||||
addCmd.Flags().StringVarP(&containerName, "container", "c", "", "The Azure container name to use for the cache")
|
||||
|
||||
9
cmd/cache/cache.go
vendored
9
cmd/cache/cache.go
vendored
@@ -18,17 +18,16 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
bucketname string
|
||||
)
|
||||
|
||||
// cacheCmd represents the cache command
|
||||
var CacheCmd = &cobra.Command{
|
||||
Use: "cache",
|
||||
Short: "For working with the cache the results of an analysis",
|
||||
Long: `Cache commands allow you to add a remote cache, list the contents of the cache, and remove items from the cache.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
err := cmd.Help()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
45
cmd/cache/get.go
vendored
Normal file
45
cmd/cache/get.go
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
)
|
||||
|
||||
// listCmd represents the list command
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get the current cache",
|
||||
Long: `Returns the current remote cache being used`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// load remote cache if it is configured
|
||||
c, err := cache.GetCacheConfiguration()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Current remote cache is: %s", c.GetName())
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
CacheCmd.AddCommand(getCmd)
|
||||
|
||||
}
|
||||
73
cmd/customAnalyzer/add.go
Normal file
73
cmd/customAnalyzer/add.go
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package customanalyzer
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
name string
|
||||
url string
|
||||
port int
|
||||
)
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Aliases: []string{"add"},
|
||||
Short: "This command will add a custom analyzer from source",
|
||||
Long: "This command allows you to add/remote/list an existing custom analyzer.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
analyzer := customAnalyzer.NewCustomAnalyzer()
|
||||
|
||||
// Check if configuration is valid
|
||||
err = analyzer.Check(configCustomAnalyzer, name, url, port)
|
||||
if err != nil {
|
||||
color.Red("Error adding custom analyzer: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
configCustomAnalyzer = append(configCustomAnalyzer, customAnalyzer.CustomAnalyzerConfiguration{
|
||||
Name: name,
|
||||
Connection: customAnalyzer.Connection{
|
||||
Url: url,
|
||||
Port: port,
|
||||
},
|
||||
})
|
||||
|
||||
viper.Set("custom_analyzers", configCustomAnalyzer)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
color.Red("Error writing config file: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
color.Green("%s added to the custom analyzers config list", name)
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
addCmd.Flags().StringVarP(&name, "name", "n", "my-custom-analyzer", "Name of the custom analyzer.")
|
||||
addCmd.Flags().StringVarP(&url, "url", "u", "localhost", "URL for the custom analyzer connection.")
|
||||
addCmd.Flags().IntVarP(&port, "port", "r", 8085, "Port for the custom analyzer connection.")
|
||||
}
|
||||
43
cmd/customAnalyzer/customAnalyzer.go
Normal file
43
cmd/customAnalyzer/customAnalyzer.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package customanalyzer
|
||||
|
||||
import (
|
||||
customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configCustomAnalyzer []customAnalyzer.CustomAnalyzerConfiguration
|
||||
|
||||
// authCmd represents the auth command
|
||||
var CustomAnalyzerCmd = &cobra.Command{
|
||||
Use: "custom-analyzer",
|
||||
Short: "Manage a custom analyzer",
|
||||
Long: `This command allows you to manage custom analyzers, including adding, removing, and listing them.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
_ = cmd.Help()
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// add subcommand to add custom analyzer
|
||||
CustomAnalyzerCmd.AddCommand(addCmd)
|
||||
// remove subcomment to remove custom analyzer
|
||||
CustomAnalyzerCmd.AddCommand(removeCmd)
|
||||
// list subcomment to list custom analyzer
|
||||
CustomAnalyzerCmd.AddCommand(listCmd)
|
||||
}
|
||||
60
cmd/customAnalyzer/list.go
Normal file
60
cmd/customAnalyzer/list.go
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package customanalyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var details bool
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured custom analyzers",
|
||||
Long: "The list command displays a list of configured custom analyzers",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// get custom_analyzers configuration
|
||||
err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get list of all Custom Analyers configured
|
||||
fmt.Print(color.YellowString("Active: \n"))
|
||||
for _, analyzer := range configCustomAnalyzer {
|
||||
fmt.Printf("> %s\n", color.GreenString(analyzer.Name))
|
||||
if details {
|
||||
printDetails(analyzer)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
listCmd.Flags().BoolVar(&details, "details", false, "Print custom analyzers configuration details")
|
||||
}
|
||||
|
||||
func printDetails(analyzer customAnalyzer.CustomAnalyzerConfiguration) {
|
||||
fmt.Printf(" - Url: %s\n", analyzer.Connection.Url)
|
||||
fmt.Printf(" - Port: %d\n", analyzer.Connection.Port)
|
||||
|
||||
}
|
||||
90
cmd/customAnalyzer/remove.go
Normal file
90
cmd/customAnalyzer/remove.go
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package customanalyzer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
names string
|
||||
)
|
||||
|
||||
var removeCmd = &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "Remove custom analyzer(s)",
|
||||
Long: "The command to remove custom analyzer(s)",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
// Ensure that the "names" flag is provided before running the command
|
||||
_ = cmd.MarkFlagRequired("names")
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if names == "" {
|
||||
// Display an error message and show command help if "names" is not set
|
||||
color.Red("Error: names must be set.")
|
||||
_ = cmd.Help()
|
||||
return
|
||||
}
|
||||
// Split the provided names by comma
|
||||
inputCustomAnalyzers := strings.Split(names, ",")
|
||||
|
||||
// Load the custom analyzers from the configuration file
|
||||
err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer)
|
||||
if err != nil {
|
||||
// Display an error message if the configuration cannot be loaded
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Iterate over each input analyzer name
|
||||
for _, inputAnalyzer := range inputCustomAnalyzers {
|
||||
foundAnalyzer := false
|
||||
// Search for the analyzer in the current configuration
|
||||
for i, analyzer := range configCustomAnalyzer {
|
||||
if analyzer.Name == inputAnalyzer {
|
||||
foundAnalyzer = true
|
||||
|
||||
// Remove the analyzer from the configuration list
|
||||
configCustomAnalyzer = append(configCustomAnalyzer[:i], configCustomAnalyzer[i+1:]...)
|
||||
color.Green("%s deleted from the custom analyzer list", analyzer.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundAnalyzer {
|
||||
// Display an error if the analyzer is not found in the configuration
|
||||
color.Red("Error: %s does not exist in configuration file. Please use k8sgpt custom-analyzer add.", inputAnalyzer)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Save the updated configuration back to the file
|
||||
viper.Set("custom_analyzers", configCustomAnalyzer)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
// Display an error if the configuration cannot be written
|
||||
color.Red("Error writing config file: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// add flag for names
|
||||
removeCmd.Flags().StringVarP(&names, "names", "n", "", "Custom analyzers to remove (separated by a comma)")
|
||||
}
|
||||
113
cmd/dump/dump.go
Normal file
113
cmd/dump/dump.go
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package dump
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/version"
|
||||
)
|
||||
|
||||
type K8sGPTInfo struct {
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
}
|
||||
type DumpOut struct {
|
||||
AIConfiguration ai.AIConfiguration
|
||||
ActiveFilters []string
|
||||
KubenetesServerVersion *version.Info
|
||||
K8sGPTInfo K8sGPTInfo
|
||||
}
|
||||
|
||||
var DumpCmd = &cobra.Command{
|
||||
Use: "dump",
|
||||
Short: "Creates a dumpfile for debugging issues with K8sGPT",
|
||||
Long: `The dump command will create a dump.*.json which will contain K8sGPT non-sensitive configuration information.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// Fetch the configuration object(s)
|
||||
// get ai configuration
|
||||
var configAI ai.AIConfiguration
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var newProvider []ai.AIProvider
|
||||
for _, config := range configAI.Providers {
|
||||
// we blank out the custom headers for data protection reasons
|
||||
config.CustomHeaders = make([]http.Header, 0)
|
||||
// blank out the password
|
||||
if len(config.Password) > 4 {
|
||||
config.Password = config.Password[:4] + "***"
|
||||
} else {
|
||||
// If the password is shorter than 4 characters
|
||||
config.Password = "***"
|
||||
}
|
||||
newProvider = append(newProvider, config)
|
||||
}
|
||||
configAI.Providers = newProvider
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
kubecontext := viper.GetString("kubecontext")
|
||||
kubeconfig := viper.GetString("kubeconfig")
|
||||
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
v, err := client.Client.Discovery().ServerVersion()
|
||||
if err != nil {
|
||||
color.Yellow("Could not find kubernetes server version")
|
||||
}
|
||||
var dumpOut DumpOut = DumpOut{
|
||||
AIConfiguration: configAI,
|
||||
ActiveFilters: activeFilters,
|
||||
KubenetesServerVersion: v,
|
||||
K8sGPTInfo: K8sGPTInfo{
|
||||
Version: viper.GetString("Version"),
|
||||
Commit: viper.GetString("Commit"),
|
||||
Date: viper.GetString("Date"),
|
||||
},
|
||||
}
|
||||
// Serialize dumpOut to JSON
|
||||
jsonData, err := json.MarshalIndent(dumpOut, "", " ")
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Write JSON data to file
|
||||
f := fmt.Sprintf("dump_%s.json", time.Now().Format("20060102150405"))
|
||||
err = os.WriteFile(f, jsonData, 0644)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
color.Green("Dump created successfully: %s", f)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
|
||||
@@ -40,10 +41,9 @@ var listCmd = &cobra.Command{
|
||||
inactiveFilters := util.SliceDiff(availableFilters, activeFilters)
|
||||
fmt.Print(color.YellowString("Active: \n"))
|
||||
for _, filter := range activeFilters {
|
||||
|
||||
// if the filter is an integration, mark this differently
|
||||
// but if the integration is inactive, remove
|
||||
if util.SliceContainsString(integrationFilters, filter) {
|
||||
if slices.Contains(integrationFilters, filter) {
|
||||
fmt.Printf("> %s\n", color.BlueString("%s (integration)", filter))
|
||||
} else {
|
||||
// This strange bit of logic will loop through every integration via
|
||||
@@ -60,13 +60,12 @@ var listCmd = &cobra.Command{
|
||||
fmt.Print(color.YellowString("Unused: \n"))
|
||||
for _, filter := range inactiveFilters {
|
||||
// if the filter is an integration, mark this differently
|
||||
if util.SliceContainsString(integrationFilters, filter) {
|
||||
if slices.Contains(integrationFilters, filter) {
|
||||
fmt.Printf("> %s\n", color.BlueString("%s (integration)", filter))
|
||||
} else {
|
||||
fmt.Printf("> %s\n", color.RedString(filter))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
@@ -15,15 +15,17 @@ package generate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
backend string
|
||||
backend string
|
||||
backendType string
|
||||
)
|
||||
|
||||
// generateCmd represents the auth command
|
||||
@@ -33,7 +35,7 @@ var GenerateCmd = &cobra.Command{
|
||||
Long: `Opens your browser to generate a key for your chosen backend.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
backendType := viper.GetString("backend_type")
|
||||
backendType = viper.GetString("backend_type")
|
||||
if backendType == "" {
|
||||
// Set the default backend
|
||||
backend = "openai"
|
||||
@@ -43,7 +45,7 @@ var GenerateCmd = &cobra.Command{
|
||||
backendType = backend
|
||||
}
|
||||
fmt.Println("")
|
||||
openbrowser("https://beta.openai.com/account/api-keys")
|
||||
openbrowser("https://platform.openai.com/api-keys")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -79,12 +81,12 @@ func openbrowser(url string) {
|
||||
func printInstructions(isGui bool, backendType string) {
|
||||
fmt.Println("")
|
||||
if isGui {
|
||||
color.Green("Opening: https://beta.openai.com/account/api-keys to generate a key for %s", backendType)
|
||||
color.Green("Opening: https://platform.openai.com/api-keys to generate a key for %s", backendType)
|
||||
fmt.Println("")
|
||||
} else {
|
||||
color.Green("Please open: https://beta.openai.com/account/api-keys to generate a key for %s", backendType)
|
||||
color.Green("Please open: https://platform.openai.com/api-keys to generate a key for %s", backendType)
|
||||
fmt.Println("")
|
||||
}
|
||||
color.Green("Please copy the generated key and run `k8sgpt auth` to add it to your config file")
|
||||
color.Green("Please copy the generated key and run `k8sgpt auth add` to add it to your config file")
|
||||
fmt.Println("")
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
skipInstall bool
|
||||
)
|
||||
var skipInstall bool
|
||||
|
||||
// activateCmd represents the activate command
|
||||
var activateCmd = &cobra.Command{
|
||||
@@ -56,5 +54,4 @@ var activateCmd = &cobra.Command{
|
||||
func init() {
|
||||
IntegrationCmd.AddCommand(activateCmd)
|
||||
activateCmd.Flags().BoolVarP(&skipInstall, "no-install", "s", false, "Only activate the integration filter without installing the filter (for example, if that filter plugin is already deployed in cluster, we do not need to re-install it again)")
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/analyze"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/auth"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/cache"
|
||||
customanalyzer "github.com/k8sgpt-ai/k8sgpt/cmd/customAnalyzer"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/dump"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/filters"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/generate"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/integration"
|
||||
@@ -56,6 +58,9 @@ func Execute(v string, c string, d string) {
|
||||
Version = v
|
||||
Commit = c
|
||||
Date = d
|
||||
viper.Set("Version", Version)
|
||||
viper.Set("Commit", Commit)
|
||||
viper.Set("Date", Date)
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
@@ -69,11 +74,13 @@ func init() {
|
||||
|
||||
rootCmd.AddCommand(auth.AuthCmd)
|
||||
rootCmd.AddCommand(analyze.AnalyzeCmd)
|
||||
rootCmd.AddCommand(dump.DumpCmd)
|
||||
rootCmd.AddCommand(filters.FiltersCmd)
|
||||
rootCmd.AddCommand(generate.GenerateCmd)
|
||||
rootCmd.AddCommand(integration.IntegrationCmd)
|
||||
rootCmd.AddCommand(serve.ServeCmd)
|
||||
rootCmd.AddCommand(cache.CacheCmd)
|
||||
rootCmd.AddCommand(customanalyzer.CustomAnalyzerCmd)
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", fmt.Sprintf("Default config file (%s/k8sgpt/k8sgpt.yaml)", xdg.ConfigHome))
|
||||
rootCmd.PersistentFlags().StringVar(&kubecontext, "kubecontext", "", "Kubernetes context to use. Only required if out-of-cluster.")
|
||||
rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
|
||||
|
||||
@@ -17,9 +17,10 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
k8sgptserver "github.com/k8sgpt-ai/k8sgpt/pkg/server"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
k8sgptserver "github.com/k8sgpt-ai/k8sgpt/pkg/server"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
@@ -27,12 +28,16 @@ import (
|
||||
|
||||
const (
|
||||
defaultTemperature float32 = 0.7
|
||||
defaultTopP float32 = 1.0
|
||||
defaultTopK int32 = 50
|
||||
defaultMaxTokens int = 2048
|
||||
)
|
||||
|
||||
var (
|
||||
port string
|
||||
metricsPort string
|
||||
backend string
|
||||
enableHttp bool
|
||||
)
|
||||
|
||||
var ServeCmd = &cobra.Command{
|
||||
@@ -66,23 +71,74 @@ var ServeCmd = &cobra.Command{
|
||||
}
|
||||
return float32(temperature)
|
||||
}
|
||||
topP := func() float32 {
|
||||
env := os.Getenv("K8SGPT_TOP_P")
|
||||
if env == "" {
|
||||
return defaultTopP
|
||||
}
|
||||
topP, err := strconv.ParseFloat(env, 32)
|
||||
if err != nil {
|
||||
color.Red("Unable to convert topP value: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if topP > 1.0 || topP < 0.0 {
|
||||
color.Red("Error: topP ranges from 0 to 1.")
|
||||
os.Exit(1)
|
||||
}
|
||||
return float32(topP)
|
||||
}
|
||||
topK := func() int32 {
|
||||
env := os.Getenv("K8SGPT_TOP_K")
|
||||
if env == "" {
|
||||
return defaultTopK
|
||||
}
|
||||
topK, err := strconv.ParseFloat(env, 32)
|
||||
if err != nil {
|
||||
color.Red("Unable to convert topK value: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if topK < 10 || topK > 100 {
|
||||
color.Red("Error: topK ranges from 1 to 100.")
|
||||
os.Exit(1)
|
||||
}
|
||||
return int32(topK)
|
||||
}
|
||||
maxTokens := func() int {
|
||||
env := os.Getenv("K8SGPT_MAX_TOKENS")
|
||||
if env == "" {
|
||||
return defaultMaxTokens
|
||||
}
|
||||
maxTokens, err := strconv.ParseInt(env, 10, 32)
|
||||
if err != nil {
|
||||
color.Red("Unable to convert maxTokens value: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return int(maxTokens)
|
||||
}
|
||||
// Check for env injection
|
||||
backend = os.Getenv("K8SGPT_BACKEND")
|
||||
password := os.Getenv("K8SGPT_PASSWORD")
|
||||
model := os.Getenv("K8SGPT_MODEL")
|
||||
baseURL := os.Getenv("K8SGPT_BASEURL")
|
||||
engine := os.Getenv("K8SGPT_ENGINE")
|
||||
proxyEndpoint := os.Getenv("K8SGPT_PROXY_ENDPOINT")
|
||||
providerId := os.Getenv("K8SGPT_PROVIDER_ID")
|
||||
// If the envs are set, allocate in place to the aiProvider
|
||||
// else exit with error
|
||||
envIsSet := backend != "" || password != "" || model != ""
|
||||
if envIsSet {
|
||||
aiProvider = &ai.AIProvider{
|
||||
Name: backend,
|
||||
Password: password,
|
||||
Model: model,
|
||||
BaseURL: baseURL,
|
||||
Engine: engine,
|
||||
Temperature: temperature(),
|
||||
Name: backend,
|
||||
Password: password,
|
||||
Model: model,
|
||||
BaseURL: baseURL,
|
||||
Engine: engine,
|
||||
ProxyEndpoint: proxyEndpoint,
|
||||
ProviderId: providerId,
|
||||
Temperature: temperature(),
|
||||
TopP: topP(),
|
||||
TopK: topK(),
|
||||
MaxTokens: maxTokens(),
|
||||
}
|
||||
|
||||
configAI.Providers = append(configAI.Providers, *aiProvider)
|
||||
@@ -110,7 +166,7 @@ var ServeCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
if aiProvider.Name == "" {
|
||||
if aiProvider == nil || aiProvider.Name == "" {
|
||||
color.Red("Error: AI provider %s not specified in configuration. Please run k8sgpt auth", backend)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -120,12 +176,18 @@ var ServeCmd = &cobra.Command{
|
||||
color.Red("failed to create logger: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer logger.Sync()
|
||||
defer func() {
|
||||
if err := logger.Sync(); err != nil {
|
||||
color.Red("failed to sync logger: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
server := k8sgptserver.Config{
|
||||
Backend: aiProvider.Name,
|
||||
Port: port,
|
||||
MetricsPort: metricsPort,
|
||||
EnableHttp: enableHttp,
|
||||
Token: aiProvider.Password,
|
||||
Logger: logger,
|
||||
}
|
||||
@@ -153,4 +215,5 @@ func init() {
|
||||
ServeCmd.Flags().StringVarP(&port, "port", "p", "8080", "Port to run the server on")
|
||||
ServeCmd.Flags().StringVarP(&metricsPort, "metrics-port", "", "8081", "Port to run the metrics-server on")
|
||||
ServeCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
|
||||
ServeCmd.Flags().BoolVarP(&enableHttp, "http", "", false, "Enable REST/http using gppc-gateway")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM golang:1.20.4-alpine3.16 AS builder
|
||||
FROM golang:1.23-alpine3.19 AS builder
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
ARG VERSION
|
||||
@@ -36,4 +36,4 @@ WORKDIR /
|
||||
COPY --from=builder /workspace/k8sgpt .
|
||||
USER 65532:65532
|
||||
|
||||
ENTRYPOINT ["/k8sgpt"]
|
||||
ENTRYPOINT ["/k8sgpt"]
|
||||
|
||||
371
go.mod
371
go.mod
@@ -1,137 +1,259 @@
|
||||
module github.com/k8sgpt-ai/k8sgpt
|
||||
|
||||
go 1.20
|
||||
go 1.23.3
|
||||
|
||||
require (
|
||||
github.com/aquasecurity/trivy-operator v0.16.4
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/magiconair/properties v1.8.7
|
||||
github.com/mittwald/go-helm-client v0.12.4
|
||||
github.com/sashabaranov/go-openai v1.17.8
|
||||
github.com/schollz/progressbar/v3 v3.14.1
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.17.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/term v0.14.0
|
||||
helm.sh/helm/v3 v3.13.2
|
||||
k8s.io/api v0.28.4
|
||||
k8s.io/apimachinery v0.28.4
|
||||
k8s.io/client-go v0.28.4
|
||||
k8s.io/kubectl v0.28.4
|
||||
github.com/aquasecurity/trivy-operator v0.22.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/kedacore/keda/v2 v2.16.0
|
||||
github.com/magiconair/properties v1.8.9
|
||||
github.com/mittwald/go-helm-client v0.12.14
|
||||
github.com/ollama/ollama v0.5.1
|
||||
github.com/sashabaranov/go-openai v1.36.0
|
||||
github.com/schollz/progressbar/v3 v3.17.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/term v0.27.0
|
||||
helm.sh/helm/v3 v3.16.3
|
||||
k8s.io/api v0.31.3
|
||||
k8s.io/apimachinery v0.31.3
|
||||
k8s.io/client-go v0.31.3
|
||||
k8s.io/kubectl v0.31.1 // indirect
|
||||
|
||||
)
|
||||
|
||||
require github.com/adrg/xdg v0.4.0
|
||||
require github.com/adrg/xdg v0.5.3
|
||||
|
||||
require (
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20231116211251-9f5041346631.2
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.28.1-20231116211251-9f5041346631.4
|
||||
cloud.google.com/go/storage v1.35.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0
|
||||
github.com/aws/aws-sdk-go v1.48.3
|
||||
github.com/cohere-ai/cohere-go v0.2.0
|
||||
buf.build/gen/go/interplex-ai/schemas/grpc/go v1.5.1-20241117203254-a91193b62179.1
|
||||
buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go v1.35.2-20241117203254-a91193b62179.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.24.0-20241118152629-1379a5a1889d.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1
|
||||
cloud.google.com/go/storage v1.48.0
|
||||
cloud.google.com/go/vertexai v0.13.2
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0
|
||||
github.com/IBM/watsonx-go v1.0.1
|
||||
github.com/aws/aws-sdk-go v1.55.6
|
||||
github.com/cohere-ai/cohere-go/v2 v2.12.2
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/generative-ai-go v0.19.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0
|
||||
github.com/hupe1980/go-huggingface v0.0.15
|
||||
github.com/kyverno/policy-reporter-kyverno-plugin v1.6.4
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
sigs.k8s.io/controller-runtime v0.16.3
|
||||
sigs.k8s.io/gateway-api v1.0.0
|
||||
google.golang.org/api v0.151.0
|
||||
github.com/oracle/oci-go-sdk/v65 v65.79.0
|
||||
github.com/prometheus/prometheus v0.300.1
|
||||
github.com/pterm/pterm v0.12.80
|
||||
google.golang.org/api v0.210.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
sigs.k8s.io/controller-runtime v0.19.3
|
||||
sigs.k8s.io/gateway-api v1.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.8 // indirect
|
||||
cloud.google.com/go/compute v1.23.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.3 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.0 // indirect
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||
github.com/cohere-ai/tokenizer v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
|
||||
atomicgo.dev/cursor v0.2.0 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
cel.dev/expr v0.16.1 // indirect
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/ai v0.8.0 // indirect
|
||||
cloud.google.com/go/aiplatform v1.69.0 // indirect
|
||||
cloud.google.com/go/auth v0.11.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.2 // indirect
|
||||
cloud.google.com/go/iam v1.2.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.2 // indirect
|
||||
cloud.google.com/go/monitoring v1.21.2 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.3 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.12.4 // indirect
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect
|
||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.0-20240603093900-cf8a8d29271d // indirect
|
||||
github.com/aquasecurity/trivy-checks v0.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.55.1 // indirect
|
||||
github.com/aws/smithy-go v1.22.0 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
|
||||
github.com/containerd/console v1.0.4 // indirect
|
||||
github.com/containerd/errdefs v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.13.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/expr-lang/expr v1.16.9 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.12.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-getter v1.7.5 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.20.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/liamg/iamgo v0.0.9 // indirect
|
||||
github.com/liamg/jfather v0.0.7 // indirect
|
||||
github.com/liamg/memoryfs v1.6.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
|
||||
github.com/masahiro331/go-disk v0.0.0-20220919035250-c8da316f91ac // indirect
|
||||
github.com/masahiro331/go-ext4-filesystem v0.0.0-20231208112839-4339555a0cd4 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/moby/buildkit v0.13.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/open-policy-agent/opa v0.65.0 // indirect
|
||||
github.com/owenrumney/squealer v1.2.2 // indirect
|
||||
github.com/package-url/packageurl-go v0.1.3 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/prometheus/common/sigv4 v0.1.0 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/segmentio/fasthash v1.0.3 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||
github.com/sony/gobreaker v0.5.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yashtewari/glob-intersection v0.2.0 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.0.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect
|
||||
mvdan.cc/sh/v3 v3.8.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/aquasecurity/defsec v0.93.1 // indirect
|
||||
github.com/aquasecurity/go-dep-parser v0.0.0-20230830122616-841bc0f812c7 // indirect
|
||||
github.com/aquasecurity/table v1.8.0 // indirect
|
||||
github.com/aquasecurity/tml v0.6.1 // indirect
|
||||
github.com/aquasecurity/trivy v0.45.1 // indirect
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20230831170347-f732860d4917 // indirect
|
||||
github.com/aquasecurity/trivy v0.53.0 // indirect
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20231020043206-3770774790ce // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/containerd/containerd v1.7.6 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.7.23 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v24.0.6+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker v24.0.7+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/cli v26.1.4+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v27.3.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
||||
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/gnostic v0.7.0
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-containerregistry v0.16.1 // indirect
|
||||
github.com/google/go-containerregistry v0.19.2 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/klauspost/compress v1.17.10 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
@@ -140,82 +262,77 @@ require (
|
||||
github.com/masahiro331/go-xfs-filesystem v0.0.0-20230608043311-a335f4599b70 // 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.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/spdystream v0.4.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rubenv/sql-migrate v1.5.2 // indirect
|
||||
github.com/rubenv/sql-migrate v1.7.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/samber/lo v1.38.1 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/samber/lo v1.39.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spdx/tools-golang v0.5.0 // indirect
|
||||
github.com/spf13/afero v1.10.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
go.opentelemetry.io/otel v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.30.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
|
||||
golang.org/x/net v0.32.0
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/grpc v1.68.1
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.28.3 // indirect
|
||||
k8s.io/apiserver v0.28.3 // indirect
|
||||
k8s.io/cli-runtime v0.28.4 // indirect
|
||||
k8s.io/component-base v0.28.4 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
oras.land/oras-go v1.2.4 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.31.3
|
||||
k8s.io/apiserver v0.31.3 // indirect
|
||||
k8s.io/cli-runtime v0.31.1 // indirect
|
||||
k8s.io/component-base v0.31.3 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 // indirect
|
||||
k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078
|
||||
oras.land/oras-go v1.2.5 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.18.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
// v1.2.0 is taken from github.com/open-policy-agent/opa v0.42.0
|
||||
// v1.2.0 incompatible with github.com/docker/docker v23.0.0-rc.1+incompatible
|
||||
replace oras.land/oras-go => oras.land/oras-go v1.2.4
|
||||
//replace oras.land/oras-go => oras.land/oras-go v1.2.4
|
||||
replace github.com/docker/docker => github.com/docker/docker v27.4.0+incompatible
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB |
@@ -2,33 +2,26 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/bedrockruntime"
|
||||
)
|
||||
|
||||
const amazonbedrockAIClientName = "amazonbedrock"
|
||||
|
||||
// AmazonBedRockClient represents the client for interacting with the Amazon Bedrock service.
|
||||
type AmazonBedRockClient struct {
|
||||
nopCloser
|
||||
|
||||
client *bedrockruntime.BedrockRuntime
|
||||
language string
|
||||
model string
|
||||
temperature float32
|
||||
}
|
||||
|
||||
// InvokeModelResponseBody represents the response body structure from the model invocation.
|
||||
type InvokeModelResponseBody struct {
|
||||
Completion string `json:"completion"`
|
||||
Stop_reason string `json:"stop_reason"`
|
||||
topP float32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
// Amazon BedRock support region list US East (N. Virginia),US West (Oregon),Asia Pacific (Singapore),Asia Pacific (Tokyo),Europe (Frankfurt)
|
||||
@@ -52,17 +45,130 @@ var BEDROCKER_SUPPORTED_REGION = []string{
|
||||
}
|
||||
|
||||
const (
|
||||
ModelAnthropicClaudeV2 = "anthropic.claude-v2"
|
||||
ModelAnthropicClaudeV1 = "anthropic.claude-v1"
|
||||
ModelAnthropicClaudeInstantV1 = "anthropic.claude-instant-v1"
|
||||
ModelAI21Jamba15Large = "ai21.jamba-1-5-large-v1:0"
|
||||
ModelAI21Jamba15Mini = "ai21.jamba-1-5-mini-v1:0"
|
||||
ModelAI21JambaInstruct = "ai21.jamba-instruct-v1:0"
|
||||
ModelAmazonNovaCanvas = "amazon.nova-canvas-v1:0"
|
||||
ModelAmazonNovaLite = "amazon.nova-lite-v1:0"
|
||||
ModelAmazonNovaMicro = "amazon.nova-micro-v1:0"
|
||||
ModelAmazonNovaPro = "amazon.nova-pro-v1:0"
|
||||
ModelAmazonNovaReel = "amazon.nova-reel-v1:0"
|
||||
ModelAmazonRerank10 = "amazon.rerank-v1:0"
|
||||
ModelAmazonTitanEmbedTextV1 = "amazon.titan-embed-text-v1"
|
||||
ModelAmazonTitanImageGenV2 = "amazon.titan-image-generator-v2:0"
|
||||
ModelAmazonTitanImageGenV1 = "amazon.titan-image-generator-v1"
|
||||
ModelAmazonTitanMultimodal = "amazon.titan-embed-image-v1"
|
||||
ModelAmazonTitanEmbedTextV2 = "amazon.titan-embed-text-v2:0"
|
||||
ModelAmazonTitanTextExpress = "amazon.titan-text-express-v1"
|
||||
ModelAmazonTitanTextLite = "amazon.titan-text-lite-v1"
|
||||
ModelAmazonTitanTextPremier = "amazon.titan-text-premier-v1:0"
|
||||
ModelAnthropicClaude3Haiku = "anthropic.claude-3-haiku-20240307-v1:0"
|
||||
ModelAnthropicClaude3Opus = "anthropic.claude-3-opus-20240229-v1:0"
|
||||
ModelAnthropicClaude3Sonnet = "anthropic.claude-3-sonnet-20240229-v1:0"
|
||||
ModelAnthropicClaude35Haiku = "anthropic.claude-3-5-haiku-20241022-v1:0"
|
||||
ModelAnthropicClaudeSonnetV3_5_V2 = "anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
ModelAnthropicClaude35Sonnet = "anthropic.claude-3-5-sonnet-20240620-v1:0"
|
||||
ModelCohereCommandLight = "cohere.command-light-text-v14"
|
||||
ModelCohereCommandRPlus = "cohere.command-r-plus-v1:0"
|
||||
ModelCohereCommandR = "cohere.command-r-v1:0"
|
||||
ModelCohereCommand = "cohere.command-text-v14"
|
||||
ModelCohereEmbedEnglish = "cohere.embed-english-v3"
|
||||
ModelCohereEmbedMultilingual = "cohere.embed-multilingual-v3"
|
||||
ModelCohereRerank35 = "cohere.rerank-v3-5:0"
|
||||
ModelLumaAIRayV2 = "luma.ray-v2:0"
|
||||
ModelMetaLlama38BInstruct = "meta.llama3-8b-instruct-v1:0"
|
||||
ModelMetaLlama370BInstruct = "meta.llama3-70b-instruct-v1:0"
|
||||
ModelMetaLlama318BInstruct = "meta.llama3-1-8b-instruct-v1:0"
|
||||
ModelMetaLlama3170BInstruct = "meta.llama3-1-70b-instruct-v1:0"
|
||||
ModelMetaLlama31405BInstruct = "meta.llama3-1-405b-instruct-v1:0"
|
||||
ModelMetaLlama321BInstruct = "meta.llama3-2-1b-instruct-v1:0"
|
||||
ModelMetaLlama323BInstruct = "meta.llama3-2-3b-instruct-v1:0"
|
||||
ModelMetaLlama3211BInstruct = "meta.llama3-2-11b-instruct-v1:0"
|
||||
ModelMetaLlama3290BInstruct = "meta.llama3-2-90b-instruct-v1:0"
|
||||
ModelMetaLlama3370BInstruct = "meta.llama3-3-70b-instruct-v1:0"
|
||||
ModelMistral7BInstruct = "mistral.mistral-7b-instruct-v0:2"
|
||||
ModelMistralLarge2402 = "mistral.mistral-large-2402-v1:0"
|
||||
ModelMistralLarge2407 = "mistral.mistral-large-2407-v1:0"
|
||||
ModelMistralSmall2402 = "mistral.mistral-small-2402-v1:0"
|
||||
ModelMistralMixtral8x7B = "mistral.mixtral-8x7b-instruct-v0:1"
|
||||
ModelStabilitySD3Large = "stability.sd3-large-v1:0"
|
||||
ModelStabilitySD35Large = "stability.sd3-5-large-v1:0"
|
||||
ModelStabilityImageCore10 = "stability.stable-image-core-v1:0"
|
||||
ModelStabilityImageCore11 = "stability.stable-image-core-v1:1"
|
||||
ModelStabilityImageUltra10 = "stability.stable-image-ultra-v1:0"
|
||||
ModelStabilityImageUltra11 = "stability.stable-image-ultra-v1:1"
|
||||
ModelAnthropicClaudeSonnetV3_5 = "anthropic.claude-3-5-sonnet-20240620-v1:0"
|
||||
ModelAnthropicClaudeV2 = "anthropic.claude-v2"
|
||||
ModelAnthropicClaudeV1 = "anthropic.claude-v1"
|
||||
ModelAnthropicClaudeInstantV1 = "anthropic.claude-instant-v1"
|
||||
ModelA21J2UltraV1 = "ai21.j2-ultra-v1"
|
||||
ModelA21J2JumboInstruct = "ai21.j2-jumbo-instruct"
|
||||
ModelAmazonTitanExpressV1 = "amazon.titan-text-express-v1"
|
||||
)
|
||||
|
||||
var BEDROCK_MODELS = []string{
|
||||
ModelAI21Jamba15Large,
|
||||
ModelAI21Jamba15Mini,
|
||||
ModelAI21JambaInstruct,
|
||||
ModelAmazonNovaCanvas,
|
||||
ModelAmazonNovaLite,
|
||||
ModelAmazonNovaMicro,
|
||||
ModelAmazonNovaPro,
|
||||
ModelAmazonNovaReel,
|
||||
ModelAmazonRerank10,
|
||||
ModelAmazonTitanEmbedTextV1,
|
||||
ModelAmazonTitanImageGenV2,
|
||||
ModelAmazonTitanImageGenV1,
|
||||
ModelAmazonTitanMultimodal,
|
||||
ModelAmazonTitanEmbedTextV2,
|
||||
ModelAmazonTitanTextExpress,
|
||||
ModelAmazonTitanTextLite,
|
||||
ModelAmazonTitanTextPremier,
|
||||
ModelAnthropicClaude3Haiku,
|
||||
ModelAnthropicClaude3Opus,
|
||||
ModelAnthropicClaude3Sonnet,
|
||||
ModelAnthropicClaude35Haiku,
|
||||
ModelAnthropicClaudeSonnetV3_5_V2, // Already in your example
|
||||
ModelAnthropicClaude35Sonnet,
|
||||
ModelCohereCommandLight,
|
||||
ModelCohereCommandRPlus,
|
||||
ModelCohereCommandR,
|
||||
ModelCohereCommand,
|
||||
ModelCohereEmbedEnglish,
|
||||
ModelCohereEmbedMultilingual,
|
||||
ModelCohereRerank35,
|
||||
ModelLumaAIRayV2,
|
||||
ModelMetaLlama38BInstruct,
|
||||
ModelMetaLlama370BInstruct,
|
||||
ModelMetaLlama318BInstruct,
|
||||
ModelMetaLlama3170BInstruct,
|
||||
ModelMetaLlama31405BInstruct,
|
||||
ModelMetaLlama321BInstruct,
|
||||
ModelMetaLlama323BInstruct,
|
||||
ModelMetaLlama3211BInstruct,
|
||||
ModelMetaLlama3290BInstruct,
|
||||
ModelMetaLlama3370BInstruct,
|
||||
ModelMistral7BInstruct,
|
||||
ModelMistralLarge2402,
|
||||
ModelMistralLarge2407,
|
||||
ModelMistralSmall2402,
|
||||
ModelMistralMixtral8x7B,
|
||||
ModelStabilitySD3Large,
|
||||
ModelStabilitySD35Large,
|
||||
ModelStabilityImageCore10,
|
||||
ModelStabilityImageCore11,
|
||||
ModelStabilityImageUltra10,
|
||||
ModelStabilityImageUltra11,
|
||||
ModelAnthropicClaudeV2,
|
||||
ModelAnthropicClaudeV1,
|
||||
ModelAnthropicClaudeInstantV1,
|
||||
ModelA21J2UltraV1,
|
||||
ModelA21J2JumboInstruct,
|
||||
ModelAmazonTitanExpressV1,
|
||||
}
|
||||
|
||||
//const TOPP = 0.9 moved to config
|
||||
|
||||
// GetModelOrDefault check config model
|
||||
func GetModelOrDefault(model string) string {
|
||||
|
||||
@@ -80,6 +186,9 @@ func GetModelOrDefault(model string) string {
|
||||
// GetModelOrDefault check config region
|
||||
func GetRegionOrDefault(region string) string {
|
||||
|
||||
if os.Getenv("AWS_DEFAULT_REGION") != "" {
|
||||
region = os.Getenv("AWS_DEFAULT_REGION")
|
||||
}
|
||||
// Check if the provided model is in the list
|
||||
for _, m := range BEDROCKER_SUPPORTED_REGION {
|
||||
if m == region {
|
||||
@@ -91,8 +200,8 @@ func GetRegionOrDefault(region string) string {
|
||||
return BEDROCK_DEFAULT_REGION
|
||||
}
|
||||
|
||||
// Configure configures the AmazonBedRockClient with the provided configuration and language.
|
||||
func (a *AmazonBedRockClient) Configure(config IAIConfig, language string) error {
|
||||
// Configure configures the AmazonBedRockClient with the provided configuration.
|
||||
func (a *AmazonBedRockClient) Configure(config IAIConfig) error {
|
||||
|
||||
// Create a new AWS session
|
||||
providerRegion := GetRegionOrDefault(config.GetProviderRegion())
|
||||
@@ -107,22 +216,50 @@ func (a *AmazonBedRockClient) Configure(config IAIConfig, language string) error
|
||||
|
||||
// Create a new BedrockRuntime client
|
||||
a.client = bedrockruntime.New(sess)
|
||||
a.language = language
|
||||
a.model = GetModelOrDefault(config.GetModel())
|
||||
a.temperature = config.GetTemperature()
|
||||
a.topP = config.GetTopP()
|
||||
a.maxTokens = config.GetMaxTokens()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompletion sends a request to the model for generating completion based on the provided prompt.
|
||||
func (a *AmazonBedRockClient) GetCompletion(ctx context.Context, prompt string, promptTmpl string) (string, error) {
|
||||
func (a *AmazonBedRockClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
|
||||
// Prepare the input data for the model invocation
|
||||
request := map[string]interface{}{
|
||||
"prompt": fmt.Sprintf("\n\nHuman: %s \n\nAssistant:", prompt),
|
||||
"max_tokens_to_sample": 1024,
|
||||
"temperature": a.temperature,
|
||||
"top_p": 0.9,
|
||||
// Prepare the input data for the model invocation based on the model & the Response Body per model as well.
|
||||
var request map[string]interface{}
|
||||
switch a.model {
|
||||
case ModelAnthropicClaudeSonnetV3_5, ModelAnthropicClaudeSonnetV3_5_V2, ModelAnthropicClaudeV2, ModelAnthropicClaudeV1, ModelAnthropicClaudeInstantV1:
|
||||
request = map[string]interface{}{
|
||||
"prompt": fmt.Sprintf("\n\nHuman: %s \n\nAssistant:", prompt),
|
||||
"max_tokens_to_sample": a.maxTokens,
|
||||
"temperature": a.temperature,
|
||||
"top_p": a.topP,
|
||||
}
|
||||
case ModelA21J2UltraV1, ModelA21J2JumboInstruct:
|
||||
request = map[string]interface{}{
|
||||
"prompt": prompt,
|
||||
"maxTokens": a.maxTokens,
|
||||
"temperature": a.temperature,
|
||||
"topP": a.topP,
|
||||
}
|
||||
case ModelAmazonTitanExpressV1:
|
||||
request = map[string]interface{}{
|
||||
"inputText": fmt.Sprintf("\n\nUser: %s", prompt),
|
||||
"textGenerationConfig": map[string]interface{}{
|
||||
"maxTokenCount": a.maxTokens,
|
||||
"temperature": a.temperature,
|
||||
"topP": a.topP,
|
||||
},
|
||||
}
|
||||
default:
|
||||
request = map[string]interface{}{
|
||||
"prompt": prompt,
|
||||
"maxTokens": a.maxTokens,
|
||||
"temperature": a.temperature,
|
||||
"topP": a.topP,
|
||||
}
|
||||
}
|
||||
|
||||
body, err := json.Marshal(request)
|
||||
@@ -143,54 +280,58 @@ func (a *AmazonBedRockClient) GetCompletion(ctx context.Context, prompt string,
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Parse the response body
|
||||
output := &InvokeModelResponseBody{}
|
||||
err = json.Unmarshal(resp.Body, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output.Completion, nil
|
||||
}
|
||||
|
||||
// Parse generates a completion for the provided prompt using the Amazon Bedrock model.
|
||||
func (a *AmazonBedRockClient) Parse(ctx context.Context, prompt []string, cache cache.ICache, promptTmpl string) (string, error) {
|
||||
inputKey := strings.Join(prompt, " ")
|
||||
// Check for cached data
|
||||
cacheKey := util.GetCacheKey(a.GetName(), a.language, inputKey)
|
||||
|
||||
if !cache.IsCacheDisabled() && cache.Exists(cacheKey) {
|
||||
response, err := cache.Load(cacheKey)
|
||||
// Response type changes as per model
|
||||
switch a.model {
|
||||
case ModelAnthropicClaudeSonnetV3_5, ModelAnthropicClaudeSonnetV3_5_V2, ModelAnthropicClaudeV2, ModelAnthropicClaudeV1, ModelAnthropicClaudeInstantV1:
|
||||
type InvokeModelResponseBody struct {
|
||||
Completion string `json:"completion"`
|
||||
Stop_reason string `json:"stop_reason"`
|
||||
}
|
||||
output := &InvokeModelResponseBody{}
|
||||
err = json.Unmarshal(resp.Body, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if response != "" {
|
||||
output, err := base64.StdEncoding.DecodeString(response)
|
||||
if err != nil {
|
||||
color.Red("error decoding cached data: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
return string(output), nil
|
||||
return output.Completion, nil
|
||||
case ModelA21J2UltraV1, ModelA21J2JumboInstruct:
|
||||
type Data struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
type Completion struct {
|
||||
Data Data `json:"data"`
|
||||
}
|
||||
type InvokeModelResponseBody struct {
|
||||
Completions []Completion `json:"completions"`
|
||||
}
|
||||
output := &InvokeModelResponseBody{}
|
||||
err = json.Unmarshal(resp.Body, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output.Completions[0].Data.Text, nil
|
||||
case ModelAmazonTitanExpressV1:
|
||||
type Result struct {
|
||||
TokenCount int `json:"tokenCount"`
|
||||
OutputText string `json:"outputText"`
|
||||
CompletionReason string `json:"completionReason"`
|
||||
}
|
||||
type InvokeModelResponseBody struct {
|
||||
InputTextTokenCount int `json:"inputTextTokenCount"`
|
||||
Results []Result `json:"results"`
|
||||
}
|
||||
output := &InvokeModelResponseBody{}
|
||||
err = json.Unmarshal(resp.Body, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output.Results[0].OutputText, nil
|
||||
default:
|
||||
return "", fmt.Errorf("model %s not supported", a.model)
|
||||
}
|
||||
|
||||
response, err := a.GetCompletion(ctx, inputKey, promptTmpl)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
|
||||
|
||||
if err != nil {
|
||||
color.Red("error storing value to cache: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the AmazonBedRockClient.
|
||||
func (a *AmazonBedRockClient) GetName() string {
|
||||
return "amazonbedrock"
|
||||
return amazonbedrockAIClientName
|
||||
}
|
||||
|
||||
@@ -15,28 +15,25 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/sagemakerruntime"
|
||||
)
|
||||
|
||||
const amazonsagemakerAIClientName = "amazonsagemaker"
|
||||
|
||||
type SageMakerAIClient struct {
|
||||
nopCloser
|
||||
|
||||
client *sagemakerruntime.SageMakerRuntime
|
||||
language string
|
||||
model string
|
||||
temperature float32
|
||||
endpoint string
|
||||
topP float32
|
||||
topK int32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
@@ -60,10 +57,11 @@ type Message struct {
|
||||
type Parameters struct {
|
||||
MaxNewTokens int `json:"max_new_tokens"`
|
||||
TopP float64 `json:"top_p"`
|
||||
TopK float64 `json:"top_k"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
func (c *SageMakerAIClient) Configure(config IAIConfig, language string) error {
|
||||
func (c *SageMakerAIClient) Configure(config IAIConfig) error {
|
||||
|
||||
// Create a new AWS session
|
||||
sess := session.Must(session.NewSessionWithOptions(session.Options{
|
||||
@@ -71,7 +69,6 @@ func (c *SageMakerAIClient) Configure(config IAIConfig, language string) error {
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
}))
|
||||
|
||||
c.language = language
|
||||
// Create a new SageMaker runtime client
|
||||
c.client = sagemakerruntime.New(sess)
|
||||
c.model = config.GetModel()
|
||||
@@ -79,27 +76,24 @@ func (c *SageMakerAIClient) Configure(config IAIConfig, language string) error {
|
||||
c.temperature = config.GetTemperature()
|
||||
c.maxTokens = config.GetMaxTokens()
|
||||
c.topP = config.GetTopP()
|
||||
c.topK = config.GetTopK()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *SageMakerAIClient) GetCompletion(ctx context.Context, prompt string, promptTmpl string) (string, error) {
|
||||
func (c *SageMakerAIClient) GetCompletion(_ context.Context, prompt string) (string, error) {
|
||||
// Create a completion request
|
||||
|
||||
if len(promptTmpl) == 0 {
|
||||
promptTmpl = PromptMap["default"]
|
||||
}
|
||||
|
||||
request := Request{
|
||||
Inputs: [][]Message{
|
||||
{
|
||||
{Role: "system", Content: "DEFAULT_PROMPT"},
|
||||
{Role: "user", Content: fmt.Sprintf(promptTmpl, c.language, prompt)},
|
||||
{Role: "user", Content: prompt},
|
||||
},
|
||||
},
|
||||
|
||||
Parameters: Parameters{
|
||||
MaxNewTokens: int(c.maxTokens),
|
||||
TopP: float64(c.topP),
|
||||
TopK: float64(c.topK),
|
||||
Temperature: float64(c.temperature),
|
||||
},
|
||||
}
|
||||
@@ -142,29 +136,6 @@ func (c *SageMakerAIClient) GetCompletion(ctx context.Context, prompt string, pr
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (a *SageMakerAIClient) Parse(ctx context.Context, prompt []string, cache cache.ICache, promptTmpl string) (string, error) {
|
||||
// parse the text with the AI backend
|
||||
inputKey := strings.Join(prompt, " ")
|
||||
// Check for cached data
|
||||
sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey))
|
||||
cacheKey := util.GetCacheKey(a.GetName(), a.language, sEnc)
|
||||
|
||||
response, err := a.GetCompletion(ctx, inputKey, promptTmpl)
|
||||
if err != nil {
|
||||
color.Red("error getting completion: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
|
||||
|
||||
if err != nil {
|
||||
color.Red("error storing value to cache: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *SageMakerAIClient) GetName() string {
|
||||
return "amazonsagemaker"
|
||||
func (c *SageMakerAIClient) GetName() string {
|
||||
return amazonsagemakerAIClientName
|
||||
}
|
||||
|
||||
@@ -2,31 +2,31 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
const azureAIClientName = "azureopenai"
|
||||
|
||||
type AzureAIClient struct {
|
||||
nopCloser
|
||||
|
||||
client *openai.Client
|
||||
language string
|
||||
model string
|
||||
temperature float32
|
||||
// organizationId string
|
||||
}
|
||||
|
||||
func (c *AzureAIClient) Configure(config IAIConfig, lang string) error {
|
||||
func (c *AzureAIClient) Configure(config IAIConfig) error {
|
||||
token := config.GetPassword()
|
||||
baseURL := config.GetBaseURL()
|
||||
engine := config.GetEngine()
|
||||
proxyEndpoint := config.GetProxyEndpoint()
|
||||
defaultConfig := openai.DefaultAzureConfig(token, baseURL)
|
||||
orgId := config.GetOrganizationId()
|
||||
|
||||
defaultConfig.AzureModelMapperFunc = func(model string) string {
|
||||
// If you use a deployment name different from the model name, you can customize the AzureModelMapperFunc function
|
||||
@@ -36,25 +36,42 @@ func (c *AzureAIClient) Configure(config IAIConfig, lang string) error {
|
||||
return azureModelMapping[model]
|
||||
|
||||
}
|
||||
|
||||
if proxyEndpoint != "" {
|
||||
proxyUrl, err := url.Parse(proxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyUrl),
|
||||
}
|
||||
|
||||
defaultConfig.HTTPClient = &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
if orgId != "" {
|
||||
defaultConfig.OrgID = orgId
|
||||
}
|
||||
|
||||
client := openai.NewClientWithConfig(defaultConfig)
|
||||
if client == nil {
|
||||
return errors.New("error creating Azure OpenAI client")
|
||||
}
|
||||
c.language = lang
|
||||
c.client = client
|
||||
c.model = config.GetModel()
|
||||
c.temperature = config.GetTemperature()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AzureAIClient) GetCompletion(ctx context.Context, prompt string, promptTmpl string) (string, error) {
|
||||
func (c *AzureAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
// Create a completion request
|
||||
resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
|
||||
Model: c.model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: fmt.Sprintf(default_prompt, c.language, prompt),
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
Temperature: c.temperature,
|
||||
@@ -65,42 +82,6 @@ func (c *AzureAIClient) GetCompletion(ctx context.Context, prompt string, prompt
|
||||
return resp.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
func (a *AzureAIClient) Parse(ctx context.Context, prompt []string, cache cache.ICache, promptTmpl string) (string, error) {
|
||||
inputKey := strings.Join(prompt, " ")
|
||||
// Check for cached data
|
||||
cacheKey := util.GetCacheKey(a.GetName(), a.language, inputKey)
|
||||
|
||||
if !cache.IsCacheDisabled() && cache.Exists(cacheKey) {
|
||||
response, err := cache.Load(cacheKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if response != "" {
|
||||
output, err := base64.StdEncoding.DecodeString(response)
|
||||
if err != nil {
|
||||
color.Red("error decoding cached data: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
}
|
||||
|
||||
response, err := a.GetCompletion(ctx, inputKey, promptTmpl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
|
||||
|
||||
if err != nil {
|
||||
color.Red("error storing value to cache: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *AzureAIClient) GetName() string {
|
||||
return "azureopenai"
|
||||
func (c *AzureAIClient) GetName() string {
|
||||
return azureAIClientName
|
||||
}
|
||||
|
||||
@@ -15,104 +15,66 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cohere-ai/cohere-go"
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
api "github.com/cohere-ai/cohere-go/v2"
|
||||
cohere "github.com/cohere-ai/cohere-go/v2/client"
|
||||
"github.com/cohere-ai/cohere-go/v2/option"
|
||||
)
|
||||
|
||||
const cohereAIClientName = "cohere"
|
||||
|
||||
type CohereClient struct {
|
||||
nopCloser
|
||||
|
||||
client *cohere.Client
|
||||
language string
|
||||
model string
|
||||
temperature float32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
func (c *CohereClient) Configure(config IAIConfig, language string) error {
|
||||
func (c *CohereClient) Configure(config IAIConfig) error {
|
||||
token := config.GetPassword()
|
||||
|
||||
client, err := cohere.CreateClient(token)
|
||||
if err != nil {
|
||||
return err
|
||||
opts := []option.RequestOption{
|
||||
cohere.WithToken(token),
|
||||
}
|
||||
|
||||
baseURL := config.GetBaseURL()
|
||||
if baseURL != "" {
|
||||
client.BaseURL = baseURL
|
||||
opts = append(opts, cohere.WithBaseURL(baseURL))
|
||||
}
|
||||
|
||||
client := cohere.NewClient(opts...)
|
||||
if client == nil {
|
||||
return errors.New("error creating Cohere client")
|
||||
}
|
||||
c.language = language
|
||||
|
||||
c.client = client
|
||||
c.model = config.GetModel()
|
||||
c.temperature = config.GetTemperature()
|
||||
c.maxTokens = config.GetMaxTokens()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CohereClient) GetCompletion(ctx context.Context, prompt, promptTmpl string) (string, error) {
|
||||
func (c *CohereClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
// Create a completion request
|
||||
if len(promptTmpl) == 0 {
|
||||
promptTmpl = PromptMap["default"]
|
||||
}
|
||||
resp, err := c.client.Generate(cohere.GenerateOptions{
|
||||
Model: c.model,
|
||||
Prompt: fmt.Sprintf(strings.TrimSpace(promptTmpl), c.language, prompt),
|
||||
MaxTokens: cohere.Uint(2048),
|
||||
Temperature: cohere.Float64(float64(c.temperature)),
|
||||
K: cohere.Int(0),
|
||||
StopSequences: []string{},
|
||||
ReturnLikelihoods: "NONE",
|
||||
response, err := c.client.Chat(ctx, &api.ChatRequest{
|
||||
Message: prompt,
|
||||
Model: &c.model,
|
||||
K: api.Int(0),
|
||||
Preamble: api.String(""),
|
||||
Temperature: api.Float64(float64(c.temperature)),
|
||||
RawPrompting: api.Bool(false),
|
||||
MaxTokens: api.Int(c.maxTokens),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Generations[0].Text, nil
|
||||
return response.Text, nil
|
||||
}
|
||||
|
||||
func (a *CohereClient) Parse(ctx context.Context, prompt []string, cache cache.ICache, promptTmpl string) (string, error) {
|
||||
inputKey := strings.Join(prompt, " ")
|
||||
// Check for cached data
|
||||
cacheKey := util.GetCacheKey(a.GetName(), a.language, inputKey)
|
||||
|
||||
if !cache.IsCacheDisabled() && cache.Exists(cacheKey) {
|
||||
response, err := cache.Load(cacheKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if response != "" {
|
||||
output, err := base64.StdEncoding.DecodeString(response)
|
||||
if err != nil {
|
||||
color.Red("error decoding cached data: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
}
|
||||
|
||||
response, err := a.GetCompletion(ctx, inputKey, promptTmpl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
|
||||
|
||||
if err != nil {
|
||||
color.Red("error storing value to cache: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *CohereClient) GetName() string {
|
||||
return "cohere"
|
||||
func (c *CohereClient) GetName() string {
|
||||
return cohereAIClientName
|
||||
}
|
||||
|
||||
122
pkg/ai/googlegenai.go
Normal file
122
pkg/ai/googlegenai.go
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
const googleAIClientName = "google"
|
||||
|
||||
type GoogleGenAIClient struct {
|
||||
client *genai.Client
|
||||
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
topK int32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
func (c *GoogleGenAIClient) Configure(config IAIConfig) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Access your API key as an environment variable (see "Set up your API key" above)
|
||||
token := config.GetPassword()
|
||||
authOption := option.WithAPIKey(token)
|
||||
if token[0] == '{' {
|
||||
authOption = option.WithCredentialsJSON([]byte(token))
|
||||
}
|
||||
|
||||
client, err := genai.NewClient(ctx, authOption)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating genai Google SDK client: %w", err)
|
||||
}
|
||||
|
||||
c.client = client
|
||||
c.model = config.GetModel()
|
||||
c.temperature = config.GetTemperature()
|
||||
c.topP = config.GetTopP()
|
||||
c.topK = config.GetTopK()
|
||||
c.maxTokens = config.GetMaxTokens()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GoogleGenAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
// Available models are at https://ai.google.dev/models e.g.gemini-pro.
|
||||
model := c.client.GenerativeModel(c.model)
|
||||
model.SetTemperature(c.temperature)
|
||||
model.SetTopP(c.topP)
|
||||
model.SetTopK(c.topK)
|
||||
model.SetMaxOutputTokens(int32(c.maxTokens))
|
||||
|
||||
// Google AI SDK is capable of different inputs than just text, for now set explicit text prompt type.
|
||||
// Similarly, we could stream the response. For now k8sgpt does not support streaming.
|
||||
resp, err := model.GenerateContent(ctx, genai.Text(prompt))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(resp.Candidates) == 0 {
|
||||
if resp.PromptFeedback.BlockReason == genai.BlockReasonSafety {
|
||||
for _, r := range resp.PromptFeedback.SafetyRatings {
|
||||
if !r.Blocked {
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("complection blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
|
||||
}
|
||||
}
|
||||
return "", errors.New("no complection returned; unknown reason")
|
||||
}
|
||||
|
||||
// Format output.
|
||||
// TODO(bwplotka): Provider richer output in certain cases e.g. suddenly finished
|
||||
// completion based on finish reasons or safety rankings.
|
||||
got := resp.Candidates[0]
|
||||
var output string
|
||||
for _, part := range got.Content.Parts {
|
||||
switch o := part.(type) {
|
||||
case genai.Text:
|
||||
output += string(o)
|
||||
output += "\n"
|
||||
default:
|
||||
color.Yellow("found unsupported AI response part of type %T; ignoring", part)
|
||||
}
|
||||
}
|
||||
|
||||
if got.CitationMetadata != nil && len(got.CitationMetadata.CitationSources) > 0 {
|
||||
output += "Citations:\n"
|
||||
for _, source := range got.CitationMetadata.CitationSources {
|
||||
// TODO(bwplotka): Give details around what exactly words could be attributed to the citation.
|
||||
output += fmt.Sprintf("* %s, %s\n", *source.URI, source.License)
|
||||
}
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (c *GoogleGenAIClient) GetName() string {
|
||||
return googleAIClientName
|
||||
}
|
||||
|
||||
func (c *GoogleGenAIClient) Close() {
|
||||
if err := c.client.Close(); err != nil {
|
||||
color.Red("googleai client close error: %v", err)
|
||||
}
|
||||
}
|
||||
181
pkg/ai/googlevertexai.go
Normal file
181
pkg/ai/googlevertexai.go
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"cloud.google.com/go/vertexai/genai"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
const googleVertexAIClientName = "googlevertexai"
|
||||
|
||||
type GoogleVertexAIClient struct {
|
||||
client *genai.Client
|
||||
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
topK int32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
// Vertex AI Gemini supported Regions
|
||||
// https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini
|
||||
const VERTEXAI_DEFAULT_REGION = "us-central1" // default use us-east-1 region
|
||||
|
||||
const (
|
||||
US_Central_1 = "us-central1"
|
||||
US_West_4 = "us-west4"
|
||||
North_America_Northeast1 = "northamerica-northeast1"
|
||||
US_East_4 = "us-east4"
|
||||
US_West_1 = "us-west1"
|
||||
Asia_Northeast_3 = "asia-northeast3"
|
||||
Asia_Southeast_1 = "asia-southeast1"
|
||||
Asia_Northeast_1 = "asia-northeast1"
|
||||
)
|
||||
|
||||
var VERTEXAI_SUPPORTED_REGION = []string{
|
||||
US_Central_1,
|
||||
US_West_4,
|
||||
North_America_Northeast1,
|
||||
US_East_4,
|
||||
US_West_1,
|
||||
Asia_Northeast_3,
|
||||
Asia_Southeast_1,
|
||||
Asia_Northeast_1,
|
||||
}
|
||||
|
||||
const (
|
||||
ModelGeminiProV1 = "gemini-1.0-pro-001"
|
||||
)
|
||||
|
||||
var VERTEXAI_MODELS = []string{
|
||||
ModelGeminiProV1,
|
||||
}
|
||||
|
||||
// GetModelOrDefault check config model
|
||||
func GetVertexAIModelOrDefault(model string) string {
|
||||
|
||||
// Check if the provided model is in the list
|
||||
for _, m := range VERTEXAI_MODELS {
|
||||
if m == model {
|
||||
return model // Return the provided model
|
||||
}
|
||||
}
|
||||
|
||||
// Return the default model if the provided model is not in the list
|
||||
return VERTEXAI_MODELS[0]
|
||||
}
|
||||
|
||||
// GetModelOrDefault check config region
|
||||
func GetVertexAIRegionOrDefault(region string) string {
|
||||
|
||||
// Check if the provided model is in the list
|
||||
for _, m := range VERTEXAI_SUPPORTED_REGION {
|
||||
if m == region {
|
||||
return region // Return the provided model
|
||||
}
|
||||
}
|
||||
|
||||
// Return the default model if the provided model is not in the list
|
||||
return VERTEXAI_DEFAULT_REGION
|
||||
}
|
||||
|
||||
func (g *GoogleVertexAIClient) Configure(config IAIConfig) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Currently you can access VertexAI either by being authenticated via OAuth or Bearer token so we need to consider both
|
||||
projectId := config.GetProviderId()
|
||||
region := GetVertexAIRegionOrDefault(config.GetProviderRegion())
|
||||
|
||||
client, err := genai.NewClient(ctx, projectId, region)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating genai Google SDK client: %w", err)
|
||||
}
|
||||
|
||||
g.client = client
|
||||
g.model = GetVertexAIModelOrDefault(config.GetModel())
|
||||
g.temperature = config.GetTemperature()
|
||||
g.topP = config.GetTopP()
|
||||
g.topK = config.GetTopK()
|
||||
g.maxTokens = config.GetMaxTokens()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GoogleVertexAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
|
||||
model := g.client.GenerativeModel(g.model)
|
||||
model.SetTemperature(g.temperature)
|
||||
model.SetTopP(g.topP)
|
||||
model.SetTopK(g.topK)
|
||||
model.SetMaxOutputTokens(int32(g.maxTokens))
|
||||
|
||||
// Google AI SDK is capable of different inputs than just text, for now set explicit text prompt type.
|
||||
// Similarly, we could stream the response. For now k8sgpt does not support streaming.
|
||||
resp, err := model.GenerateContent(ctx, genai.Text(prompt))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(resp.Candidates) == 0 {
|
||||
if resp.PromptFeedback.BlockReason > 0 {
|
||||
for _, r := range resp.PromptFeedback.SafetyRatings {
|
||||
if !r.Blocked {
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("complection blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
|
||||
}
|
||||
}
|
||||
return "", errors.New("no complection returned; unknown reason")
|
||||
}
|
||||
|
||||
// Format output.
|
||||
// TODO(bwplotka): Provider richer output in certain cases e.g. suddenly finished
|
||||
// completion based on finish reasons or safety rankings.
|
||||
got := resp.Candidates[0]
|
||||
var output string
|
||||
for _, part := range got.Content.Parts {
|
||||
switch o := part.(type) {
|
||||
case genai.Text:
|
||||
output += string(o)
|
||||
output += "\n"
|
||||
default:
|
||||
color.Yellow("found unsupported AI response part of type %T; ignoring", part)
|
||||
}
|
||||
}
|
||||
|
||||
if got.CitationMetadata != nil && len(got.CitationMetadata.Citations) > 0 {
|
||||
output += "Citations:\n"
|
||||
for _, source := range got.CitationMetadata.Citations {
|
||||
// TODO(bwplotka): Give details around what exactly words could be attributed to the citation.
|
||||
output += fmt.Sprintf("* %s, %s\n", source.URI, source.License)
|
||||
}
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (g *GoogleVertexAIClient) GetName() string {
|
||||
return googleVertexAIClientName
|
||||
}
|
||||
|
||||
func (g *GoogleVertexAIClient) Close() {
|
||||
if err := g.client.Close(); err != nil {
|
||||
color.Red("googleai client close error: %v", err)
|
||||
}
|
||||
}
|
||||
63
pkg/ai/huggingface.go
Normal file
63
pkg/ai/huggingface.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hupe1980/go-huggingface"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
const huggingfaceAIClientName = "huggingface"
|
||||
|
||||
type HuggingfaceClient struct {
|
||||
nopCloser
|
||||
|
||||
client *huggingface.InferenceClient
|
||||
model string
|
||||
topP float32
|
||||
topK int32
|
||||
temperature float32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
func (c *HuggingfaceClient) Configure(config IAIConfig) error {
|
||||
token := config.GetPassword()
|
||||
|
||||
client := huggingface.NewInferenceClient(token)
|
||||
|
||||
c.client = client
|
||||
c.model = config.GetModel()
|
||||
c.topP = config.GetTopP()
|
||||
c.topK = config.GetTopK()
|
||||
c.temperature = config.GetTemperature()
|
||||
if config.GetMaxTokens() > 500 {
|
||||
c.maxTokens = 500
|
||||
} else {
|
||||
c.maxTokens = config.GetMaxTokens()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HuggingfaceClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
resp, err := c.client.Conversational(ctx, &huggingface.ConversationalRequest{
|
||||
Inputs: huggingface.ConverstationalInputs{
|
||||
Text: prompt,
|
||||
},
|
||||
Model: c.model,
|
||||
Parameters: huggingface.ConversationalParameters{
|
||||
TopP: ptr.To[float64](float64(c.topP)),
|
||||
TopK: ptr.To[int](int(c.topK)),
|
||||
Temperature: ptr.To[float64](float64(c.temperature)),
|
||||
MaxLength: &c.maxTokens,
|
||||
},
|
||||
Options: huggingface.Options{
|
||||
WaitForModel: ptr.To[bool](true),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GeneratedText, nil
|
||||
}
|
||||
|
||||
func (c *HuggingfaceClient) GetName() string { return huggingfaceAIClientName }
|
||||
105
pkg/ai/iai.go
105
pkg/ai/iai.go
@@ -15,8 +15,7 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,39 +23,68 @@ var (
|
||||
&OpenAIClient{},
|
||||
&AzureAIClient{},
|
||||
&LocalAIClient{},
|
||||
&OllamaClient{},
|
||||
&NoOpAIClient{},
|
||||
&CohereClient{},
|
||||
&AmazonBedRockClient{},
|
||||
&SageMakerAIClient{},
|
||||
&GoogleGenAIClient{},
|
||||
&HuggingfaceClient{},
|
||||
&GoogleVertexAIClient{},
|
||||
&OCIGenAIClient{},
|
||||
&IBMWatsonxAIClient{},
|
||||
}
|
||||
Backends = []string{
|
||||
"openai",
|
||||
"localai",
|
||||
"azureopenai",
|
||||
"noopai",
|
||||
"cohere",
|
||||
"amazonbedrock",
|
||||
"amazonsagemaker",
|
||||
openAIClientName,
|
||||
localAIClientName,
|
||||
ollamaClientName,
|
||||
azureAIClientName,
|
||||
cohereAIClientName,
|
||||
amazonbedrockAIClientName,
|
||||
amazonsagemakerAIClientName,
|
||||
googleAIClientName,
|
||||
noopAIClientName,
|
||||
huggingfaceAIClientName,
|
||||
googleVertexAIClientName,
|
||||
ociClientName,
|
||||
ibmWatsonxAIClientName,
|
||||
}
|
||||
)
|
||||
|
||||
// IAI is an interface all clients (representing backends) share.
|
||||
type IAI interface {
|
||||
Configure(config IAIConfig, language string) error
|
||||
GetCompletion(ctx context.Context, prompt string, promptTmpl string) (string, error)
|
||||
Parse(ctx context.Context, prompt []string, cache cache.ICache, promptTmpl string) (string, error)
|
||||
// Configure sets up client for given configuration. This is expected to be
|
||||
// executed once per client life-time (e.g. analysis CLI command invocation).
|
||||
Configure(config IAIConfig) error
|
||||
// GetCompletion generates text based on prompt.
|
||||
GetCompletion(ctx context.Context, prompt string) (string, error)
|
||||
// GetName returns name of the backend/client.
|
||||
GetName() string
|
||||
// Close cleans all the resources. No other methods should be used on the
|
||||
// objects after this method is invoked.
|
||||
Close()
|
||||
}
|
||||
|
||||
type nopCloser struct{}
|
||||
|
||||
func (nopCloser) Close() {}
|
||||
|
||||
type IAIConfig interface {
|
||||
GetPassword() string
|
||||
GetModel() string
|
||||
GetBaseURL() string
|
||||
GetProxyEndpoint() string
|
||||
GetEndpointName() string
|
||||
GetEngine() string
|
||||
GetTemperature() float32
|
||||
GetProviderRegion() string
|
||||
GetTopP() float32
|
||||
GetTopK() int32
|
||||
GetMaxTokens() int
|
||||
GetProviderId() string
|
||||
GetCompartmentId() string
|
||||
GetOrganizationId() string
|
||||
GetCustomHeaders() []http.Header
|
||||
}
|
||||
|
||||
func NewClient(provider string) IAI {
|
||||
@@ -75,22 +103,33 @@ type AIConfiguration struct {
|
||||
}
|
||||
|
||||
type AIProvider struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Model string `mapstructure:"model"`
|
||||
Password string `mapstructure:"password" yaml:"password,omitempty"`
|
||||
BaseURL string `mapstructure:"baseurl" yaml:"baseurl,omitempty"`
|
||||
EndpointName string `mapstructure:"endpointname" yaml:"endpointname,omitempty"`
|
||||
Engine string `mapstructure:"engine" yaml:"engine,omitempty"`
|
||||
Temperature float32 `mapstructure:"temperature" yaml:"temperature,omitempty"`
|
||||
ProviderRegion string `mapstructure:"providerregion" yaml:"providerregion,omitempty"`
|
||||
TopP float32 `mapstructure:"topp" yaml:"topp,omitempty"`
|
||||
MaxTokens int `mapstructure:"maxtokens" yaml:"maxtokens,omitempty"`
|
||||
Name string `mapstructure:"name"`
|
||||
Model string `mapstructure:"model"`
|
||||
Password string `mapstructure:"password" yaml:"password,omitempty"`
|
||||
BaseURL string `mapstructure:"baseurl" yaml:"baseurl,omitempty"`
|
||||
ProxyEndpoint string `mapstructure:"proxyEndpoint" yaml:"proxyEndpoint,omitempty"`
|
||||
ProxyPort string `mapstructure:"proxyPort" yaml:"proxyPort,omitempty"`
|
||||
EndpointName string `mapstructure:"endpointname" yaml:"endpointname,omitempty"`
|
||||
Engine string `mapstructure:"engine" yaml:"engine,omitempty"`
|
||||
Temperature float32 `mapstructure:"temperature" yaml:"temperature,omitempty"`
|
||||
ProviderRegion string `mapstructure:"providerregion" yaml:"providerregion,omitempty"`
|
||||
ProviderId string `mapstructure:"providerid" yaml:"providerid,omitempty"`
|
||||
CompartmentId string `mapstructure:"compartmentid" yaml:"compartmentid,omitempty"`
|
||||
TopP float32 `mapstructure:"topp" yaml:"topp,omitempty"`
|
||||
TopK int32 `mapstructure:"topk" yaml:"topk,omitempty"`
|
||||
MaxTokens int `mapstructure:"maxtokens" yaml:"maxtokens,omitempty"`
|
||||
OrganizationId string `mapstructure:"organizationid" yaml:"organizationid,omitempty"`
|
||||
CustomHeaders []http.Header `mapstructure:"customHeaders"`
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetBaseURL() string {
|
||||
return p.BaseURL
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetProxyEndpoint() string {
|
||||
return p.ProxyEndpoint
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetEndpointName() string {
|
||||
return p.EndpointName
|
||||
}
|
||||
@@ -99,6 +138,10 @@ func (p *AIProvider) GetTopP() float32 {
|
||||
return p.TopP
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetTopK() int32 {
|
||||
return p.TopK
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetMaxTokens() int {
|
||||
return p.MaxTokens
|
||||
}
|
||||
@@ -122,7 +165,23 @@ func (p *AIProvider) GetProviderRegion() string {
|
||||
return p.ProviderRegion
|
||||
}
|
||||
|
||||
var passwordlessProviders = []string{"localai", "amazonsagemaker", "amazonbedrock"}
|
||||
func (p *AIProvider) GetProviderId() string {
|
||||
return p.ProviderId
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetCompartmentId() string {
|
||||
return p.CompartmentId
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetOrganizationId() string {
|
||||
return p.OrganizationId
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetCustomHeaders() []http.Header {
|
||||
return p.CustomHeaders
|
||||
}
|
||||
|
||||
var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "googlevertexai", "oci"}
|
||||
|
||||
func NeedPassword(backend string) bool {
|
||||
for _, b := range passwordlessProviders {
|
||||
|
||||
67
pkg/ai/interactive/interactive.go
Normal file
67
pkg/ai/interactive/interactive.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package interactive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
|
||||
"github.com/pterm/pterm"
|
||||
)
|
||||
|
||||
type INTERACTIVE_STATE int
|
||||
|
||||
const (
|
||||
prompt = "Given the following context: "
|
||||
)
|
||||
|
||||
const (
|
||||
E_RUNNING INTERACTIVE_STATE = iota
|
||||
E_EXITED = iota
|
||||
)
|
||||
|
||||
type InteractionRunner struct {
|
||||
config *analysis.Analysis
|
||||
State chan INTERACTIVE_STATE
|
||||
contextWindow []byte
|
||||
}
|
||||
|
||||
func NewInteractionRunner(config *analysis.Analysis, contextWindow []byte) *InteractionRunner {
|
||||
return &InteractionRunner{
|
||||
config: config,
|
||||
contextWindow: contextWindow,
|
||||
State: make(chan INTERACTIVE_STATE),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InteractionRunner) StartInteraction() {
|
||||
a.State <- E_RUNNING
|
||||
pterm.Println("Interactive mode enabled [type exit to close.]")
|
||||
for {
|
||||
|
||||
query := pterm.DefaultInteractiveTextInput.WithMultiLine(false)
|
||||
queryString, err := query.Show()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
if queryString == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(queryString, "exit") {
|
||||
a.State <- E_EXITED
|
||||
continue
|
||||
}
|
||||
pterm.Println()
|
||||
contextWindow := fmt.Sprintf("%s %s %s", prompt, string(a.contextWindow),
|
||||
queryString)
|
||||
|
||||
response, err := a.config.AIClient.GetCompletion(a.config.Context,
|
||||
contextWindow)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
a.State <- E_EXITED
|
||||
continue
|
||||
}
|
||||
pterm.Println(response)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package ai
|
||||
|
||||
const localAIClientName = "localai"
|
||||
|
||||
type LocalAIClient struct {
|
||||
OpenAIClient
|
||||
}
|
||||
|
||||
func (a *LocalAIClient) GetName() string {
|
||||
return "localai"
|
||||
return localAIClientName
|
||||
}
|
||||
|
||||
@@ -15,58 +15,23 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
)
|
||||
|
||||
const noopAIClientName = "noopai"
|
||||
|
||||
type NoOpAIClient struct {
|
||||
client string
|
||||
language string
|
||||
model string
|
||||
nopCloser
|
||||
}
|
||||
|
||||
func (c *NoOpAIClient) Configure(config IAIConfig, language string) error {
|
||||
token := config.GetPassword()
|
||||
c.language = language
|
||||
c.client = fmt.Sprintf("I am a noop client with the token %s ", token)
|
||||
c.model = config.GetModel()
|
||||
func (c *NoOpAIClient) Configure(_ IAIConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NoOpAIClient) GetCompletion(ctx context.Context, prompt string, promptTmpl string) (string, error) {
|
||||
// Create a completion request
|
||||
func (c *NoOpAIClient) GetCompletion(_ context.Context, prompt string) (string, error) {
|
||||
response := "I am a noop response to the prompt " + prompt
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *NoOpAIClient) Parse(ctx context.Context, prompt []string, cache cache.ICache, promptTmpl string) (string, error) {
|
||||
// parse the text with the AI backend
|
||||
inputKey := strings.Join(prompt, " ")
|
||||
// Check for cached data
|
||||
sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey))
|
||||
cacheKey := util.GetCacheKey(a.GetName(), a.language, sEnc)
|
||||
|
||||
response, err := a.GetCompletion(ctx, inputKey, promptTmpl)
|
||||
if err != nil {
|
||||
color.Red("error getting completion: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
|
||||
|
||||
if err != nil {
|
||||
color.Red("error storing value to cache: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *NoOpAIClient) GetName() string {
|
||||
return "noopai"
|
||||
func (c *NoOpAIClient) GetName() string {
|
||||
return noopAIClientName
|
||||
}
|
||||
|
||||
97
pkg/ai/ocigenai.go
Normal file
97
pkg/ai/ocigenai.go
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
Copyright 2024 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/oracle/oci-go-sdk/v65/common"
|
||||
"github.com/oracle/oci-go-sdk/v65/generativeaiinference"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const ociClientName = "oci"
|
||||
|
||||
type OCIGenAIClient struct {
|
||||
nopCloser
|
||||
|
||||
client *generativeaiinference.GenerativeAiInferenceClient
|
||||
model string
|
||||
compartmentId string
|
||||
temperature float32
|
||||
topP float32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) GetName() string {
|
||||
return ociClientName
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) Configure(config IAIConfig) error {
|
||||
config.GetEndpointName()
|
||||
c.model = config.GetModel()
|
||||
c.temperature = config.GetTemperature()
|
||||
c.topP = config.GetTopP()
|
||||
c.maxTokens = config.GetMaxTokens()
|
||||
c.compartmentId = config.GetCompartmentId()
|
||||
provider := common.DefaultConfigProvider()
|
||||
client, err := generativeaiinference.NewGenerativeAiInferenceClientWithConfigurationProvider(provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.client = &client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
generateTextRequest := c.newGenerateTextRequest(prompt)
|
||||
generateTextResponse, err := c.client.GenerateText(ctx, generateTextRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return extractGeneratedText(generateTextResponse.InferenceResponse)
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) newGenerateTextRequest(prompt string) generativeaiinference.GenerateTextRequest {
|
||||
temperatureF64 := float64(c.temperature)
|
||||
topPF64 := float64(c.topP)
|
||||
return generativeaiinference.GenerateTextRequest{
|
||||
GenerateTextDetails: generativeaiinference.GenerateTextDetails{
|
||||
CompartmentId: &c.compartmentId,
|
||||
ServingMode: generativeaiinference.OnDemandServingMode{
|
||||
ModelId: &c.model,
|
||||
},
|
||||
InferenceRequest: generativeaiinference.CohereLlmInferenceRequest{
|
||||
Prompt: &prompt,
|
||||
MaxTokens: &c.maxTokens,
|
||||
Temperature: &temperatureF64,
|
||||
TopP: &topPF64,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func extractGeneratedText(llmInferenceResponse generativeaiinference.LlmInferenceResponse) (string, error) {
|
||||
response, ok := llmInferenceResponse.(generativeaiinference.CohereLlmInferenceResponse)
|
||||
if !ok {
|
||||
return "", errors.New("failed to extract generated text from backed response")
|
||||
}
|
||||
sb := strings.Builder{}
|
||||
for _, text := range response.GeneratedTexts {
|
||||
if text.Text != nil {
|
||||
sb.WriteString(*text.Text)
|
||||
}
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
102
pkg/ai/ollama.go
Normal file
102
pkg/ai/ollama.go
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
ollama "github.com/ollama/ollama/api"
|
||||
)
|
||||
|
||||
const ollamaClientName = "ollama"
|
||||
|
||||
type OllamaClient struct {
|
||||
nopCloser
|
||||
|
||||
client *ollama.Client
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
}
|
||||
|
||||
const (
|
||||
defaultBaseURL = "http://localhost:11434"
|
||||
defaultModel = "llama3"
|
||||
)
|
||||
|
||||
func (c *OllamaClient) Configure(config IAIConfig) error {
|
||||
baseURL := config.GetBaseURL()
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
}
|
||||
baseClientURL, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxyEndpoint := config.GetProxyEndpoint()
|
||||
httpClient := http.DefaultClient
|
||||
if proxyEndpoint != "" {
|
||||
proxyUrl, err := url.Parse(proxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyUrl),
|
||||
}
|
||||
|
||||
httpClient = &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
c.client = ollama.NewClient(baseClientURL, httpClient)
|
||||
if c.client == nil {
|
||||
return errors.New("error creating Ollama client")
|
||||
}
|
||||
c.model = config.GetModel()
|
||||
if c.model == "" {
|
||||
c.model = defaultModel
|
||||
}
|
||||
c.temperature = config.GetTemperature()
|
||||
c.topP = config.GetTopP()
|
||||
return nil
|
||||
}
|
||||
func (c *OllamaClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
req := &ollama.GenerateRequest{
|
||||
Model: c.model,
|
||||
Prompt: prompt,
|
||||
Stream: new(bool),
|
||||
Options: map[string]interface{}{
|
||||
"temperature": c.temperature,
|
||||
"top_p": c.topP,
|
||||
},
|
||||
}
|
||||
completion := ""
|
||||
respFunc := func(resp ollama.GenerateResponse) error {
|
||||
completion = resp.Response
|
||||
return nil
|
||||
}
|
||||
err := c.client.Generate(ctx, req, respFunc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return completion, nil
|
||||
}
|
||||
func (a *OllamaClient) GetName() string {
|
||||
return ollamaClientName
|
||||
}
|
||||
104
pkg/ai/openai.go
104
pkg/ai/openai.go
@@ -15,24 +15,23 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
const openAIClientName = "openai"
|
||||
|
||||
type OpenAIClient struct {
|
||||
nopCloser
|
||||
|
||||
client *openai.Client
|
||||
language string
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
// organizationId string
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -40,47 +39,66 @@ const (
|
||||
maxToken = 2048
|
||||
presencePenalty = 0.0
|
||||
frequencyPenalty = 0.0
|
||||
topP = 1.0
|
||||
)
|
||||
|
||||
func (c *OpenAIClient) Configure(config IAIConfig, language string) error {
|
||||
func (c *OpenAIClient) Configure(config IAIConfig) error {
|
||||
token := config.GetPassword()
|
||||
defaultConfig := openai.DefaultConfig(token)
|
||||
orgId := config.GetOrganizationId()
|
||||
proxyEndpoint := config.GetProxyEndpoint()
|
||||
|
||||
baseURL := config.GetBaseURL()
|
||||
if baseURL != "" {
|
||||
defaultConfig.BaseURL = baseURL
|
||||
}
|
||||
|
||||
transport := &http.Transport{}
|
||||
if proxyEndpoint != "" {
|
||||
proxyUrl, err := url.Parse(proxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transport.Proxy = http.ProxyURL(proxyUrl)
|
||||
}
|
||||
|
||||
if orgId != "" {
|
||||
defaultConfig.OrgID = orgId
|
||||
}
|
||||
|
||||
customHeaders := config.GetCustomHeaders()
|
||||
defaultConfig.HTTPClient = &http.Client{
|
||||
Transport: &OpenAIHeaderTransport{
|
||||
Origin: transport,
|
||||
Headers: customHeaders,
|
||||
},
|
||||
}
|
||||
|
||||
client := openai.NewClientWithConfig(defaultConfig)
|
||||
if client == nil {
|
||||
return errors.New("error creating OpenAI client")
|
||||
}
|
||||
c.language = language
|
||||
c.client = client
|
||||
c.model = config.GetModel()
|
||||
c.temperature = config.GetTemperature()
|
||||
c.topP = config.GetTopP()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *OpenAIClient) GetCompletion(ctx context.Context, prompt string, promptTmpl string) (string, error) {
|
||||
func (c *OpenAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
// Create a completion request
|
||||
if len(promptTmpl) == 0 {
|
||||
promptTmpl = PromptMap["default"]
|
||||
}
|
||||
resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
|
||||
Model: c.model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: fmt.Sprintf(promptTmpl, c.language, prompt),
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
Temperature: c.temperature,
|
||||
MaxTokens: maxToken,
|
||||
PresencePenalty: presencePenalty,
|
||||
FrequencyPenalty: frequencyPenalty,
|
||||
TopP: topP,
|
||||
TopP: c.topP,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -88,42 +106,28 @@ func (c *OpenAIClient) GetCompletion(ctx context.Context, prompt string, promptT
|
||||
return resp.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
func (a *OpenAIClient) Parse(ctx context.Context, prompt []string, cache cache.ICache, promptTmpl string) (string, error) {
|
||||
inputKey := strings.Join(prompt, " ")
|
||||
// Check for cached data
|
||||
cacheKey := util.GetCacheKey(a.GetName(), a.language, inputKey)
|
||||
func (c *OpenAIClient) GetName() string {
|
||||
return openAIClientName
|
||||
}
|
||||
|
||||
if !cache.IsCacheDisabled() && cache.Exists(cacheKey) {
|
||||
response, err := cache.Load(cacheKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// OpenAIHeaderTransport is an http.RoundTripper that adds the given headers to each request.
|
||||
type OpenAIHeaderTransport struct {
|
||||
Origin http.RoundTripper
|
||||
Headers []http.Header
|
||||
}
|
||||
|
||||
if response != "" {
|
||||
output, err := base64.StdEncoding.DecodeString(response)
|
||||
if err != nil {
|
||||
color.Red("error decoding cached data: %v", err)
|
||||
return "", nil
|
||||
// RoundTrip implements the http.RoundTripper interface.
|
||||
func (t *OpenAIHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Clone the request to avoid modifying the original request
|
||||
clonedReq := req.Clone(req.Context())
|
||||
for _, header := range t.Headers {
|
||||
for key, values := range header {
|
||||
// Possible values per header: RFC 2616
|
||||
for _, value := range values {
|
||||
clonedReq.Header.Add(key, value)
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
}
|
||||
|
||||
response, err := a.GetCompletion(ctx, inputKey, promptTmpl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
|
||||
|
||||
if err != nil {
|
||||
color.Red("error storing value to cache: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *OpenAIClient) GetName() string {
|
||||
return "openai"
|
||||
return t.Origin.RoundTrip(clonedReq)
|
||||
}
|
||||
|
||||
106
pkg/ai/openai_header_transport_test.go
Normal file
106
pkg/ai/openai_header_transport_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Mock configuration
|
||||
type mockConfig struct {
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetPassword() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetOrganizationId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetProxyEndpoint() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetBaseURL() string {
|
||||
return m.baseURL
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetCustomHeaders() []http.Header {
|
||||
return []http.Header{
|
||||
{"X-Custom-Header-1": []string{"Value1"}},
|
||||
{"X-Custom-Header-2": []string{"Value2"}},
|
||||
{"X-Custom-Header-2": []string{"Value3"}}, // Testing multiple values for the same header
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetModel() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetTemperature() float32 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetTopP() float32 {
|
||||
return 0.0
|
||||
}
|
||||
func (m *mockConfig) GetCompartmentId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetTopK() int32 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetMaxTokens() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetEndpointName() string {
|
||||
return ""
|
||||
}
|
||||
func (m *mockConfig) GetEngine() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetProviderId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockConfig) GetProviderRegion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestOpenAIClient_CustomHeaders(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "Value1", r.Header.Get("X-Custom-Header-1"))
|
||||
assert.ElementsMatch(t, []string{"Value2", "Value3"}, r.Header["X-Custom-Header-2"])
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Mock response for openai completion
|
||||
mockResponse := `{"choices": [{"message": {"content": "test"}}]}`
|
||||
n, err := w.Write([]byte(mockResponse))
|
||||
if err != nil {
|
||||
t.Fatalf("error writing response: %v", err)
|
||||
}
|
||||
if n != len(mockResponse) {
|
||||
t.Fatalf("expected to write %d bytes but wrote %d bytes", len(mockResponse), n)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := &mockConfig{baseURL: server.URL}
|
||||
|
||||
client := &OpenAIClient{}
|
||||
err := client.Configure(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Make a completion request to trigger the headers
|
||||
ctx := context.Background()
|
||||
_, err = client.GetCompletion(ctx, "foo prompt")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -8,10 +8,64 @@ const (
|
||||
`
|
||||
trivy_vuln_prompt = "Explain the following trivy scan result and the detail risk or root cause of the CVE ID, then provide a solution. Response in %s: %s"
|
||||
trivy_conf_prompt = "Explain the following trivy scan result and the detail risk or root cause of the security check, then provide a solution."
|
||||
|
||||
prom_conf_prompt = `Simplify the following Prometheus error message delimited by triple dashes written in --- %s --- language; --- %s ---.
|
||||
This error came when validating the Prometheus configuration file.
|
||||
Provide step by step instructions to fix, with suggestions, referencing Prometheus documentation if relevant.
|
||||
Write the output in the following format in no more than 300 characters:
|
||||
Error: {Explain error here}
|
||||
Solution: {Step by step solution here}
|
||||
`
|
||||
|
||||
prom_relabel_prompt = `
|
||||
Return your prompt in this language: %s, beginning with
|
||||
The following is a list of the form:
|
||||
job_name:
|
||||
{Prometheus job_name}
|
||||
relabel_configs:
|
||||
{Prometheus relabel_configs}
|
||||
kubernetes_sd_configs:
|
||||
{Prometheus service discovery config}
|
||||
---
|
||||
%s
|
||||
---
|
||||
For each job_name, describe the Kubernetes service and pod labels,
|
||||
namespaces, ports, and containers they match.
|
||||
Return the message:
|
||||
Discovered and parsed Prometheus scrape configurations.
|
||||
For targets to be scraped by Prometheus, ensure they are running with
|
||||
at least one of the following label sets:
|
||||
Then for each job, write this format:
|
||||
- Job: {job_name}
|
||||
- Service Labels:
|
||||
- {list of service labels}
|
||||
- Pod Labels:
|
||||
- {list of pod labels}
|
||||
- Namespaces:
|
||||
- {list of namespaces}
|
||||
- Ports:
|
||||
- {list of ports}
|
||||
- Containers:
|
||||
- {list of container names}
|
||||
`
|
||||
|
||||
kyverno_prompt = `Simplify the following Kyverno warnings message delimited by triple dashes written in --- %s --- language; --- %s ---.
|
||||
Provide the most probable solution as a kubectl command.
|
||||
|
||||
Write the output in the following format, for the solution, only show the kubectl command:
|
||||
|
||||
Error: {Explain error here}
|
||||
|
||||
Solution: {kubectl command}
|
||||
`
|
||||
)
|
||||
|
||||
var PromptMap = map[string]string{
|
||||
"default": default_prompt,
|
||||
"VulnerabilityReport": trivy_vuln_prompt, // for Trivy integration, the key should match `Result.Kind` in pkg/common/types.go
|
||||
"ConfigAuditReport": trivy_conf_prompt,
|
||||
"default": default_prompt,
|
||||
"VulnerabilityReport": trivy_vuln_prompt, // for Trivy integration, the key should match `Result.Kind` in pkg/common/types.go
|
||||
"ConfigAuditReport": trivy_conf_prompt,
|
||||
"PrometheusConfigValidate": prom_conf_prompt,
|
||||
"PrometheusConfigRelabelReport": prom_relabel_prompt,
|
||||
"PolicyReport": kyverno_prompt,
|
||||
"ClusterPolicyReport": kyverno_prompt,
|
||||
}
|
||||
|
||||
86
pkg/ai/watsonxai.go
Normal file
86
pkg/ai/watsonxai.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
wx "github.com/IBM/watsonx-go/pkg/models"
|
||||
)
|
||||
|
||||
const ibmWatsonxAIClientName = "ibmwatsonxai"
|
||||
|
||||
type IBMWatsonxAIClient struct {
|
||||
nopCloser
|
||||
|
||||
client *wx.Client
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
topK int32
|
||||
maxNewTokens int
|
||||
}
|
||||
|
||||
const (
|
||||
modelMetallama = "ibm/granite-13b-chat-v2"
|
||||
maxTokens = 2048
|
||||
)
|
||||
|
||||
func (c *IBMWatsonxAIClient) Configure(config IAIConfig) error {
|
||||
if config.GetModel() == "" {
|
||||
c.model = modelMetallama
|
||||
} else {
|
||||
c.model = config.GetModel()
|
||||
}
|
||||
if config.GetMaxTokens() == 0 {
|
||||
c.maxNewTokens = maxTokens
|
||||
} else {
|
||||
c.maxNewTokens = config.GetMaxTokens()
|
||||
}
|
||||
c.temperature = config.GetTemperature()
|
||||
c.topP = config.GetTopP()
|
||||
c.topK = config.GetTopK()
|
||||
|
||||
apiKey := config.GetPassword()
|
||||
if apiKey == "" {
|
||||
return errors.New("No watsonx API key provided")
|
||||
}
|
||||
|
||||
projectId := config.GetProviderId()
|
||||
if projectId == "" {
|
||||
return errors.New("No watsonx project ID provided")
|
||||
}
|
||||
|
||||
client, err := wx.NewClient(
|
||||
wx.WithWatsonxAPIKey(apiKey),
|
||||
wx.WithWatsonxProjectID(projectId),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create client for testing. Error: %v", err)
|
||||
}
|
||||
c.client = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *IBMWatsonxAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
result, err := c.client.GenerateText(
|
||||
c.model,
|
||||
prompt,
|
||||
wx.WithTemperature((float64)(c.temperature)),
|
||||
wx.WithTopP((float64)(c.topP)),
|
||||
wx.WithTopK((uint)(c.topK)),
|
||||
wx.WithMaxNewTokens((uint)(c.maxNewTokens)),
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Expected no error, but got an error: %v", err)
|
||||
}
|
||||
if result.Text == "" {
|
||||
return "", errors.New("Expected a result, but got an empty string")
|
||||
}
|
||||
return result.Text, nil
|
||||
}
|
||||
|
||||
func (c *IBMWatsonxAIClient) GetName() string {
|
||||
return ibmWatsonxAIClientName
|
||||
}
|
||||
@@ -15,12 +15,12 @@ package analysis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
openapi_v2 "github.com/google/gnostic/openapiv2"
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/custom"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
@@ -38,19 +39,25 @@ type Analysis struct {
|
||||
Context context.Context
|
||||
Filters []string
|
||||
Client *kubernetes.Client
|
||||
Language string
|
||||
AIClient ai.IAI
|
||||
Results []common.Result
|
||||
Errors []string
|
||||
Namespace string
|
||||
LabelSelector string
|
||||
Cache cache.ICache
|
||||
Explain bool
|
||||
MaxConcurrency int
|
||||
AnalysisAIProvider string // The name of the AI Provider used for this analysis
|
||||
WithDoc bool
|
||||
WithStats bool
|
||||
Stats []common.AnalysisStats
|
||||
}
|
||||
|
||||
type AnalysisStatus string
|
||||
type AnalysisErrors []string
|
||||
type (
|
||||
AnalysisStatus string
|
||||
AnalysisErrors []string
|
||||
)
|
||||
|
||||
const (
|
||||
StateOK AnalysisStatus = "OK"
|
||||
@@ -65,25 +72,75 @@ type JsonOutput struct {
|
||||
Results []common.Result `json:"results"`
|
||||
}
|
||||
|
||||
func NewAnalysis(backend string, language string, filters []string, namespace string, noCache bool, explain bool, maxConcurrency int, withDoc bool) (*Analysis, error) {
|
||||
var configAI ai.AIConfiguration
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
func NewAnalysis(
|
||||
backend string,
|
||||
language string,
|
||||
filters []string,
|
||||
namespace string,
|
||||
labelSelector string,
|
||||
noCache bool,
|
||||
explain bool,
|
||||
maxConcurrency int,
|
||||
withDoc bool,
|
||||
interactiveMode bool,
|
||||
httpHeaders []string,
|
||||
withStats bool,
|
||||
) (*Analysis, error) {
|
||||
// Get kubernetes client from viper.
|
||||
kubecontext := viper.GetString("kubecontext")
|
||||
kubeconfig := viper.GetString("kubeconfig")
|
||||
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
return nil, fmt.Errorf("initialising kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
if len(configAI.Providers) == 0 && explain {
|
||||
color.Red("Error: AI provider not specified in configuration. Please run k8sgpt auth")
|
||||
os.Exit(1)
|
||||
// Load remote cache if it is configured.
|
||||
cache, err := cache.GetCacheConfiguration()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if noCache {
|
||||
cache.DisableCache()
|
||||
}
|
||||
|
||||
a := &Analysis{
|
||||
Context: context.Background(),
|
||||
Filters: filters,
|
||||
Client: client,
|
||||
Language: language,
|
||||
Namespace: namespace,
|
||||
LabelSelector: labelSelector,
|
||||
Cache: cache,
|
||||
Explain: explain,
|
||||
MaxConcurrency: maxConcurrency,
|
||||
WithDoc: withDoc,
|
||||
WithStats: withStats,
|
||||
}
|
||||
if !explain {
|
||||
// Return early if AI use was not requested.
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var configAI ai.AIConfiguration
|
||||
if err := viper.UnmarshalKey("ai", &configAI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(configAI.Providers) == 0 {
|
||||
return nil, errors.New("AI provider not specified in configuration. Please run k8sgpt auth")
|
||||
}
|
||||
|
||||
// Backend string will have high priority than a default provider
|
||||
// Backend as "openai" represents the default CLI argument passed through
|
||||
if configAI.DefaultProvider != "" && backend == "openai" {
|
||||
// Hence, use the default provider only if the backend is not specified by the user.
|
||||
if configAI.DefaultProvider != "" && backend == "" {
|
||||
backend = configAI.DefaultProvider
|
||||
}
|
||||
|
||||
if backend == "" {
|
||||
backend = "openai"
|
||||
}
|
||||
|
||||
var aiProvider ai.AIProvider
|
||||
for _, provider := range configAI.Providers {
|
||||
if backend == provider.Name {
|
||||
@@ -93,49 +150,71 @@ func NewAnalysis(backend string, language string, filters []string, namespace st
|
||||
}
|
||||
|
||||
if aiProvider.Name == "" {
|
||||
color.Red("Error: AI provider %s not specified in configuration. Please run k8sgpt auth", backend)
|
||||
return nil, errors.New("AI provider not specified in configuration")
|
||||
return nil, fmt.Errorf("AI provider %s not specified in configuration. Please run k8sgpt auth", backend)
|
||||
}
|
||||
|
||||
aiClient := ai.NewClient(aiProvider.Name)
|
||||
if err := aiClient.Configure(&aiProvider, language); err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
customHeaders := util.NewHeaders(httpHeaders)
|
||||
aiProvider.CustomHeaders = customHeaders
|
||||
if err := aiClient.Configure(&aiProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.AIClient = aiClient
|
||||
a.AnalysisAIProvider = aiProvider.Name
|
||||
return a, nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// Get kubernetes client from viper
|
||||
func (a *Analysis) CustomAnalyzersAreAvailable() bool {
|
||||
var customAnalyzers []custom.CustomAnalyzer
|
||||
if err := viper.UnmarshalKey("custom_analyzers", &customAnalyzers); err != nil {
|
||||
return false
|
||||
}
|
||||
return len(customAnalyzers) > 0
|
||||
}
|
||||
|
||||
kubecontext := viper.GetString("kubecontext")
|
||||
kubeconfig := viper.GetString("kubeconfig")
|
||||
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
|
||||
if err != nil {
|
||||
color.Red("Error initialising kubernetes client: %v", err)
|
||||
return nil, err
|
||||
func (a *Analysis) RunCustomAnalysis() {
|
||||
var customAnalyzers []custom.CustomAnalyzer
|
||||
if err := viper.UnmarshalKey("custom_analyzers", &customAnalyzers); err != nil {
|
||||
a.Errors = append(a.Errors, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// load remote cache if it is configured
|
||||
cache, err := cache.GetCacheConfiguration()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
semaphore := make(chan struct{}, a.MaxConcurrency)
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
for _, cAnalyzer := range customAnalyzers {
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
go func(analyzer custom.CustomAnalyzer, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
defer wg.Done()
|
||||
canClient, err := custom.NewClient(cAnalyzer.Connection)
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("Client creation error for %s analyzer", cAnalyzer.Name))
|
||||
mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if noCache {
|
||||
cache.DisableCache()
|
||||
result, err := canClient.Run()
|
||||
if result.Kind == "" {
|
||||
// for custom analyzer name, we must use a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.',
|
||||
//and must start and end with an alphanumeric character (e.g. 'example.com',
|
||||
//regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')
|
||||
result.Kind = cAnalyzer.Name
|
||||
}
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", cAnalyzer.Name, err))
|
||||
mutex.Unlock()
|
||||
} else {
|
||||
mutex.Lock()
|
||||
a.Results = append(a.Results, result)
|
||||
mutex.Unlock()
|
||||
}
|
||||
<-semaphore
|
||||
}(cAnalyzer, &wg, semaphore)
|
||||
}
|
||||
|
||||
return &Analysis{
|
||||
Context: ctx,
|
||||
Filters: filters,
|
||||
Client: client,
|
||||
AIClient: aiClient,
|
||||
Namespace: namespace,
|
||||
Cache: cache,
|
||||
Explain: explain,
|
||||
MaxConcurrency: maxConcurrency,
|
||||
AnalysisAIProvider: backend,
|
||||
WithDoc: withDoc,
|
||||
}, nil
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (a *Analysis) RunAnalysis() {
|
||||
@@ -158,58 +237,32 @@ func (a *Analysis) RunAnalysis() {
|
||||
Client: a.Client,
|
||||
Context: a.Context,
|
||||
Namespace: a.Namespace,
|
||||
LabelSelector: a.LabelSelector,
|
||||
AIClient: a.AIClient,
|
||||
OpenapiSchema: openapiSchema,
|
||||
}
|
||||
|
||||
semaphore := make(chan struct{}, a.MaxConcurrency)
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
// if there are no filters selected and no active_filters then run coreAnalyzer
|
||||
if len(a.Filters) == 0 && len(activeFilters) == 0 {
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
for _, analyzer := range coreAnalyzerMap {
|
||||
for name, analyzer := range coreAnalyzerMap {
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
go func(analyzer common.IAnalyzer, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
defer wg.Done()
|
||||
results, err := analyzer.Analyze(analyzerConfig)
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", reflect.TypeOf(analyzer).Name(), err))
|
||||
mutex.Unlock()
|
||||
}
|
||||
mutex.Lock()
|
||||
a.Results = append(a.Results, results...)
|
||||
mutex.Unlock()
|
||||
<-semaphore
|
||||
}(analyzer, &wg, semaphore)
|
||||
go a.executeAnalyzer(analyzer, name, analyzerConfig, semaphore, &wg, &mutex)
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
semaphore = make(chan struct{}, a.MaxConcurrency)
|
||||
// if the filters flag is specified
|
||||
if len(a.Filters) != 0 {
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
for _, filter := range a.Filters {
|
||||
if analyzer, ok := analyzerMap[filter]; ok {
|
||||
semaphore <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func(analyzer common.IAnalyzer, filter string) {
|
||||
defer wg.Done()
|
||||
results, err := analyzer.Analyze(analyzerConfig)
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
|
||||
mutex.Unlock()
|
||||
}
|
||||
mutex.Lock()
|
||||
a.Results = append(a.Results, results...)
|
||||
mutex.Unlock()
|
||||
<-semaphore
|
||||
}(analyzer, filter)
|
||||
go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex)
|
||||
} else {
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("\"%s\" filter does not exist. Please run k8sgpt filters list.", filter))
|
||||
}
|
||||
@@ -218,32 +271,57 @@ func (a *Analysis) RunAnalysis() {
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
semaphore = make(chan struct{}, a.MaxConcurrency)
|
||||
// use active_filters
|
||||
for _, filter := range activeFilters {
|
||||
if analyzer, ok := analyzerMap[filter]; ok {
|
||||
semaphore <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func(analyzer common.IAnalyzer, filter string) {
|
||||
defer wg.Done()
|
||||
results, err := analyzer.Analyze(analyzerConfig)
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
|
||||
mutex.Unlock()
|
||||
}
|
||||
mutex.Lock()
|
||||
a.Results = append(a.Results, results...)
|
||||
mutex.Unlock()
|
||||
<-semaphore
|
||||
}(analyzer, filter)
|
||||
go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, analyzerConfig common.Analyzer, semaphore chan struct{}, wg *sync.WaitGroup, mutex *sync.Mutex) {
|
||||
defer wg.Done()
|
||||
|
||||
var startTime time.Time
|
||||
var elapsedTime time.Duration
|
||||
|
||||
// Start the timer
|
||||
if a.WithStats {
|
||||
startTime = time.Now()
|
||||
}
|
||||
|
||||
// Run the analyzer
|
||||
results, err := analyzer.Analyze(analyzerConfig)
|
||||
|
||||
// Measure the time taken
|
||||
if a.WithStats {
|
||||
elapsedTime = time.Since(startTime)
|
||||
}
|
||||
stat := common.AnalysisStats{
|
||||
Analyzer: filter,
|
||||
DurationTime: elapsedTime,
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
if a.WithStats {
|
||||
a.Stats = append(a.Stats, stat)
|
||||
}
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
|
||||
} else {
|
||||
if a.WithStats {
|
||||
a.Stats = append(a.Stats, stat)
|
||||
}
|
||||
a.Results = append(a.Results, results...)
|
||||
}
|
||||
<-semaphore
|
||||
}
|
||||
|
||||
func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
if len(a.Results) == 0 {
|
||||
return nil
|
||||
@@ -265,14 +343,14 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
}
|
||||
texts = append(texts, failure.Text)
|
||||
}
|
||||
// If the resource `Kind` comes from a "integration plugin", maybe a customized prompt template will be involved.
|
||||
var promptTemplate string
|
||||
|
||||
promptTemplate := ai.PromptMap["default"]
|
||||
// If the resource `Kind` comes from an "integration plugin",
|
||||
// maybe a customized prompt template will be involved.
|
||||
if prompt, ok := ai.PromptMap[analysis.Kind]; ok {
|
||||
promptTemplate = prompt
|
||||
} else {
|
||||
promptTemplate = ai.PromptMap["default"]
|
||||
}
|
||||
parsedText, err := a.AIClient.Parse(a.Context, texts, a.Cache, promptTemplate)
|
||||
result, err := a.getAIResultForSanitizedFailures(texts, promptTemplate)
|
||||
if err != nil {
|
||||
// FIXME: can we avoid checking if output is json multiple times?
|
||||
// maybe implement the progress bar better?
|
||||
@@ -280,23 +358,22 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
_ = bar.Exit()
|
||||
}
|
||||
|
||||
// Check for exhaustion
|
||||
// Check for exhaustion.
|
||||
if strings.Contains(err.Error(), "status code: 429") {
|
||||
return fmt.Errorf("exhausted API quota for AI provider %s: %v", a.AIClient.GetName(), err)
|
||||
} else {
|
||||
return fmt.Errorf("failed while calling AI provider %s: %v", a.AIClient.GetName(), err)
|
||||
}
|
||||
return fmt.Errorf("failed while calling AI provider %s: %v", a.AIClient.GetName(), err)
|
||||
}
|
||||
|
||||
if anonymize {
|
||||
for _, failure := range analysis.Error {
|
||||
for _, s := range failure.Sensitive {
|
||||
parsedText = strings.ReplaceAll(parsedText, s.Masked, s.Unmasked)
|
||||
result = strings.ReplaceAll(result, s.Masked, s.Unmasked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analysis.Details = parsedText
|
||||
analysis.Details = result
|
||||
if output != "json" {
|
||||
_ = bar.Add(1)
|
||||
}
|
||||
@@ -304,3 +381,44 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Analysis) getAIResultForSanitizedFailures(texts []string, promptTmpl string) (string, error) {
|
||||
inputKey := strings.Join(texts, " ")
|
||||
// Check for cached data.
|
||||
// TODO(bwplotka): This might depend on model too (or even other client configuration pieces), fix it in later PRs.
|
||||
cacheKey := util.GetCacheKey(a.AIClient.GetName(), a.Language, inputKey)
|
||||
|
||||
if !a.Cache.IsCacheDisabled() && a.Cache.Exists(cacheKey) {
|
||||
response, err := a.Cache.Load(cacheKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if response != "" {
|
||||
output, err := base64.StdEncoding.DecodeString(response)
|
||||
if err == nil {
|
||||
return string(output), nil
|
||||
}
|
||||
color.Red("error decoding cached data; ignoring cache item: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process template.
|
||||
prompt := fmt.Sprintf(strings.TrimSpace(promptTmpl), a.Language, inputKey)
|
||||
response, err := a.AIClient.GetCompletion(a.Context, prompt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = a.Cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response))); err != nil {
|
||||
color.Red("error storing value to cache; value won't be cached: %v", err)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (a *Analysis) Close() {
|
||||
if a.AIClient == nil {
|
||||
return
|
||||
}
|
||||
a.AIClient.Close()
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/magiconair/properties/assert"
|
||||
@@ -26,8 +31,6 @@ import (
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// sub-function
|
||||
@@ -85,6 +88,7 @@ func analysis_RunAnalysisFilterTester(t *testing.T, filterFlag string) []common.
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
WithDoc: true,
|
||||
}
|
||||
if len(filterFlag) > 0 {
|
||||
// `--filter` is explicitly given
|
||||
@@ -133,6 +137,10 @@ func TestAnalysis_RunAnalysisActiveFilter(t *testing.T) {
|
||||
viper.SetDefault("active_filters", []string{"Ingress", "Service", "Pod"})
|
||||
results = analysis_RunAnalysisFilterTester(t, "")
|
||||
assert.Equal(t, len(results), 3)
|
||||
|
||||
// Invalid filter
|
||||
results = analysis_RunAnalysisFilterTester(t, "invalid")
|
||||
assert.Equal(t, len(results), 0)
|
||||
}
|
||||
|
||||
func TestAnalysis_NoProblemJsonOutput(t *testing.T) {
|
||||
@@ -279,3 +287,120 @@ func TestAnalysis_MultipleProblemJsonOutput(t *testing.T) {
|
||||
|
||||
require.Equal(t, got, expected)
|
||||
}
|
||||
|
||||
func TestNewAnalysis(t *testing.T) {
|
||||
disabledCache := cache.New("disabled-cache")
|
||||
disabledCache.DisableCache()
|
||||
aiClient := &ai.NoOpAIClient{}
|
||||
results := []common.Result{
|
||||
{
|
||||
Kind: "VulnerabilityReport",
|
||||
Error: []common.Failure{
|
||||
{
|
||||
Text: "This is a custom failure",
|
||||
KubernetesDoc: "test-kubernetes-doc",
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Masked: "masked-error",
|
||||
Unmasked: "unmasked-error",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a Analysis
|
||||
output string
|
||||
anonymize bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "Empty results",
|
||||
a: Analysis{},
|
||||
},
|
||||
{
|
||||
name: "cache disabled",
|
||||
a: Analysis{
|
||||
AIClient: aiClient,
|
||||
Cache: disabledCache,
|
||||
Results: results,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "output and anonymize both set",
|
||||
a: Analysis{
|
||||
AIClient: aiClient,
|
||||
Cache: cache.New("test-cache"),
|
||||
Results: results,
|
||||
},
|
||||
output: "test-output",
|
||||
anonymize: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.a.GetAIResults(tt.output, tt.anonymize)
|
||||
if tt.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAIResultForSanitizedFailures(t *testing.T) {
|
||||
enabledCache := cache.New("enabled-cache")
|
||||
disabledCache := cache.New("disabled-cache")
|
||||
disabledCache.DisableCache()
|
||||
aiClient := &ai.NoOpAIClient{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a Analysis
|
||||
texts []string
|
||||
promptTmpl string
|
||||
expectedOutput string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "Cache enabled",
|
||||
a: Analysis{
|
||||
AIClient: aiClient,
|
||||
Cache: enabledCache,
|
||||
},
|
||||
texts: []string{"some-data"},
|
||||
expectedOutput: "I am a noop response to the prompt %!(EXTRA string=, string=some-data)",
|
||||
},
|
||||
{
|
||||
name: "cache disabled",
|
||||
a: Analysis{
|
||||
AIClient: aiClient,
|
||||
Cache: disabledCache,
|
||||
Language: "English",
|
||||
},
|
||||
texts: []string{"test input"},
|
||||
promptTmpl: "Response in %s: %s",
|
||||
expectedOutput: "I am a noop response to the prompt Response in English: test input",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := tt.a.getAIResultForSanitizedFailures(tt.texts, tt.promptTmpl)
|
||||
if tt.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expectedOutput, output)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.expectedErr)
|
||||
require.Empty(t, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,11 +55,27 @@ func (a *Analysis) jsonOutput() ([]byte, error) {
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (a *Analysis) PrintStats() []byte {
|
||||
var output strings.Builder
|
||||
|
||||
output.WriteString(color.YellowString("The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.\n"))
|
||||
|
||||
for _, stat := range a.Stats {
|
||||
output.WriteString(fmt.Sprintf("- Analyzer %s took %s \n", color.YellowString(stat.Analyzer), stat.DurationTime))
|
||||
}
|
||||
|
||||
return []byte(output.String())
|
||||
}
|
||||
|
||||
func (a *Analysis) textOutput() ([]byte, error) {
|
||||
var output strings.Builder
|
||||
|
||||
// Print the AI provider used for this analysis
|
||||
output.WriteString(fmt.Sprintf("AI Provider: %s\n", color.YellowString(a.AnalysisAIProvider)))
|
||||
// Print the AI provider used for this analysis (if explain was enabled).
|
||||
if a.Explain {
|
||||
output.WriteString(fmt.Sprintf("AI Provider: %s\n", color.YellowString(a.AnalysisAIProvider)))
|
||||
} else {
|
||||
output.WriteString(fmt.Sprintf("AI Provider: %s\n", color.YellowString("AI not used; --explain not set")))
|
||||
}
|
||||
|
||||
if len(a.Errors) != 0 {
|
||||
output.WriteString("\n")
|
||||
@@ -74,8 +90,10 @@ func (a *Analysis) textOutput() ([]byte, error) {
|
||||
return []byte(output.String()), nil
|
||||
}
|
||||
for n, result := range a.Results {
|
||||
output.WriteString(fmt.Sprintf("%s %s(%s)\n", color.CyanString("%d", n),
|
||||
color.YellowString(result.Name), color.CyanString(result.ParentObject)))
|
||||
output.WriteString(fmt.Sprintf("%s: %s %s(%s)\n", color.CyanString("%d", n),
|
||||
color.HiYellowString(result.Kind),
|
||||
color.YellowString(result.Name),
|
||||
color.CyanString(result.ParentObject)))
|
||||
for _, err := range result.Error {
|
||||
output.WriteString(fmt.Sprintf("- %s %s\n", color.RedString("Error:"), color.RedString(err.Text)))
|
||||
if err.KubernetesDoc != "" {
|
||||
|
||||
64
pkg/analysis/output_test.go
Normal file
64
pkg/analysis/output_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2024 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPrintOutput(t *testing.T) {
|
||||
require.NotEmpty(t, getOutputFormats())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a *Analysis
|
||||
format string
|
||||
expectedOutput string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "json format",
|
||||
a: &Analysis{},
|
||||
format: "json",
|
||||
expectedOutput: "{\n \"provider\": \"\",\n \"errors\": null,\n \"status\": \"OK\",\n \"problems\": 0,\n \"results\": null\n}",
|
||||
},
|
||||
{
|
||||
name: "text format",
|
||||
a: &Analysis{},
|
||||
format: "text",
|
||||
expectedOutput: "AI Provider: AI not used; --explain not set\n\nNo problems detected\n",
|
||||
},
|
||||
{
|
||||
name: "unsupported format",
|
||||
a: &Analysis{},
|
||||
format: "unsupported",
|
||||
expectedErr: "unsupported output format: unsupported. Available format",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output, err := tt.a.PrintOutput(tt.format)
|
||||
if tt.expectedErr == "" {
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(output), tt.expectedOutput)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.expectedErr)
|
||||
require.Nil(t, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, err
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
cronJobList, err := a.Client.GetClient().BatchV1().CronJobs(a.Namespace).List(a.Context, v1.ListOptions{})
|
||||
cronJobList, err := a.Client.GetClient().BatchV1().CronJobs(a.Namespace).List(a.Context, v1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -123,15 +123,15 @@ func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, err
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, cronJob.Name, cronJob.Namespace).Set(float64(len(failures)))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range preAnalysis {
|
||||
currentAnalysis := common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
for key, value := range preAnalysis {
|
||||
currentAnalysis := common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
|
||||
@@ -15,219 +15,186 @@ package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestCronJobSuccess(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-cronjob",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"analysisDate": "2022-04-01",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/1 * * * *",
|
||||
ConcurrencyPolicy: "Allow",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "example-container",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
RestartPolicy: v1.RestartPolicyOnFailure,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
func TestCronJobAnalyzer(t *testing.T) {
|
||||
suspend := new(bool)
|
||||
*suspend = true
|
||||
|
||||
invalidStartingDeadline := new(int64)
|
||||
*invalidStartingDeadline = -7
|
||||
|
||||
validStartingDeadline := new(int64)
|
||||
*validStartingDeadline = 7
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
analyzer := CronJobAnalyzer{}
|
||||
analysisResults, err := analyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(analysisResults), 0)
|
||||
}
|
||||
|
||||
func TestCronJobBroken(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-cronjob",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"analysisDate": "2022-04-01",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*** * * * *",
|
||||
ConcurrencyPolicy: "Allow",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "example-container",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
RestartPolicy: v1.RestartPolicyOnFailure,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
analyzer := CronJobAnalyzer{}
|
||||
analysisResults, err := analyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
assert.Equal(t, analysisResults[0].Name, "default/example-cronjob")
|
||||
assert.Equal(t, analysisResults[0].Kind, "CronJob")
|
||||
}
|
||||
|
||||
func TestCronJobBrokenMultipleNamespaceFiltering(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-cronjob",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"analysisDate": "2022-04-01",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*** * * * *",
|
||||
ConcurrencyPolicy: "Allow",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ1",
|
||||
// This CronJob won't be list because of namespace filtering.
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ2",
|
||||
Namespace: "default",
|
||||
},
|
||||
// A suspended CronJob will contribute to failures.
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Suspend: suspend,
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ3",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
// Valid schedule
|
||||
Schedule: "*/1 * * * *",
|
||||
|
||||
// Negative starting deadline
|
||||
StartingDeadlineSeconds: invalidStartingDeadline,
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ4",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
// Invalid schedule
|
||||
Schedule: "*** * * * *",
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ5",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
// Valid schedule
|
||||
Schedule: "*/1 * * * *",
|
||||
|
||||
// Positive starting deadline shouldn't be any problem.
|
||||
StartingDeadlineSeconds: validStartingDeadline,
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
// This cronjob shouldn't contribute to any failures.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "successful-cronjob",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"analysisDate": "2022-04-01",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "example-container",
|
||||
Image: "nginx",
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/1 * * * *",
|
||||
ConcurrencyPolicy: "Allow",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "example-container",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
RestartPolicy: v1.RestartPolicyOnFailure,
|
||||
},
|
||||
},
|
||||
RestartPolicy: v1.RestartPolicyOnFailure,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-cronjob",
|
||||
Namespace: "other-namespace",
|
||||
Annotations: map[string]string{
|
||||
"analysisDate": "2022-04-01",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*** * * * *",
|
||||
ConcurrencyPolicy: "Allow",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "example-container",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
RestartPolicy: v1.RestartPolicyOnFailure,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
analyzer := CronJobAnalyzer{}
|
||||
analysisResults, err := analyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
cjAnalyzer := CronJobAnalyzer{}
|
||||
results, err := cjAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []string{
|
||||
"default/CJ2",
|
||||
"default/CJ3",
|
||||
"default/CJ4",
|
||||
}
|
||||
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
assert.Equal(t, analysisResults[0].Name, "default/example-cronjob")
|
||||
assert.Equal(t, analysisResults[0].Kind, "CronJob")
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i], result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronJobAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
suspend := new(bool)
|
||||
*suspend = true
|
||||
|
||||
invalidStartingDeadline := new(int64)
|
||||
*invalidStartingDeadline = -7
|
||||
|
||||
validStartingDeadline := new(int64)
|
||||
*validStartingDeadline = 7
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "cronjob",
|
||||
},
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=cronjob",
|
||||
}
|
||||
|
||||
cjAnalyzer := CronJobAnalyzer{}
|
||||
results, err := cjAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/CJ1", results[0].Name)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
deployments, err := a.Client.GetClient().AppsV1().Deployments(a.Namespace).List(context.Background(), v1.ListOptions{})
|
||||
deployments, err := a.Client.GetClient().AppsV1().Deployments(a.Namespace).List(context.Background(), v1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -151,3 +151,55 @@ func TestDeploymentAnalyzerNamespaceFiltering(t *testing.T) {
|
||||
assert.Equal(t, analysisResults[0].Kind, "Deployment")
|
||||
assert.Equal(t, analysisResults[0].Name, "default/example")
|
||||
}
|
||||
|
||||
func TestDeploymentAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "deployment",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: func() *int32 { i := int32(3); return &i }(),
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: func() *int32 { i := int32(3); return &i }(),
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=deployment",
|
||||
}
|
||||
|
||||
deploymentAnalyzer := DeploymentAnalyzer{}
|
||||
analysisResults, err := deploymentAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func FetchLatestEvent(ctx context.Context, kubernetesClient *kubernetes.Client, namespace string, name string) (*v1.Event, error) {
|
||||
|
||||
// get the list of events
|
||||
events, err := kubernetesClient.GetClient().CoreV1().Events(namespace).List(ctx,
|
||||
metav1.ListOptions{
|
||||
FieldSelector: "involvedObject.name=" + name,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// find most recent event
|
||||
var latestEvent *v1.Event
|
||||
for _, event := range events.Items {
|
||||
if latestEvent == nil {
|
||||
// this is required, as a pointer to a loop variable would always yield the latest value in the range
|
||||
e := event
|
||||
latestEvent = &e
|
||||
}
|
||||
if event.LastTimestamp.After(latestEvent.LastTimestamp.Time) {
|
||||
// this is required, as a pointer to a loop variable would always yield the latest value in the range
|
||||
e := event
|
||||
latestEvent = &e
|
||||
}
|
||||
}
|
||||
return latestEvent, nil
|
||||
}
|
||||
@@ -37,8 +37,13 @@ func (GatewayAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
gtwList := >wapi.GatewayList{}
|
||||
gc := >wapi.GatewayClass{}
|
||||
client := a.Client.CtrlClient
|
||||
gtwapi.AddToScheme(client.Scheme())
|
||||
if err := client.List(a.Context, gtwList, &ctrl.ListOptions{}); err != nil {
|
||||
err := gtwapi.AddToScheme(client.Scheme())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labelSelector := util.LabelStrToSelector(a.LabelSelector)
|
||||
if err := client.List(a.Context, gtwList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,13 @@ func BuildGatewayClass(name string) gtwapi.GatewayClass {
|
||||
return GatewayClass
|
||||
}
|
||||
|
||||
func BuildGateway(className gtwapi.ObjectName, status metav1.ConditionStatus) gtwapi.Gateway {
|
||||
func BuildGateway(className gtwapi.ObjectName, status metav1.ConditionStatus, labels map[string]string) gtwapi.Gateway {
|
||||
Gateway := gtwapi.Gateway{}
|
||||
Gateway.Name = "foobar"
|
||||
Gateway.Namespace = "default"
|
||||
if labels != nil {
|
||||
Gateway.Labels = labels
|
||||
}
|
||||
Gateway.Spec.GatewayClassName = className
|
||||
Gateway.Spec.Listeners = []gtwapi.Listener{
|
||||
{
|
||||
@@ -53,11 +56,18 @@ func TestGatewayAnalyzer(t *testing.T) {
|
||||
AcceptedStatus := metav1.ConditionTrue
|
||||
GatewayClass := BuildGatewayClass(string(ClassName))
|
||||
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus)
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus, nil)
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
scheme := scheme.Scheme
|
||||
gtwapi.Install(scheme)
|
||||
apiextensionsv1.AddToScheme(scheme)
|
||||
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
objects := []runtime.Object{
|
||||
&Gateway,
|
||||
&GatewayClass,
|
||||
@@ -84,12 +94,18 @@ func TestGatewayAnalyzer(t *testing.T) {
|
||||
func TestMissingClassGatewayAnalyzer(t *testing.T) {
|
||||
ClassName := gtwapi.ObjectName("non-existed")
|
||||
AcceptedStatus := metav1.ConditionTrue
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus)
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus, nil)
|
||||
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
scheme := scheme.Scheme
|
||||
gtwapi.Install(scheme)
|
||||
apiextensionsv1.AddToScheme(scheme)
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
objects := []runtime.Object{
|
||||
&Gateway,
|
||||
}
|
||||
@@ -117,12 +133,18 @@ func TestStatusGatewayAnalyzer(t *testing.T) {
|
||||
AcceptedStatus := metav1.ConditionUnknown
|
||||
GatewayClass := BuildGatewayClass(string(ClassName))
|
||||
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus)
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus, nil)
|
||||
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
scheme := scheme.Scheme
|
||||
gtwapi.Install(scheme)
|
||||
apiextensionsv1.AddToScheme(scheme)
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
objects := []runtime.Object{
|
||||
&Gateway,
|
||||
&GatewayClass,
|
||||
@@ -159,3 +181,70 @@ func TestStatusGatewayAnalyzer(t *testing.T) {
|
||||
t.Errorf("Expected message, <%v> , not found in Gateway's analysis results", want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGatewayAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
ClassName := gtwapi.ObjectName("non-existed")
|
||||
AcceptedStatus := metav1.ConditionTrue
|
||||
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus, map[string]string{"app": "gateway"})
|
||||
scheme := scheme.Scheme
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
objects := []runtime.Object{
|
||||
&Gateway,
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
|
||||
|
||||
analyzerInstance := GatewayAnalyzer{}
|
||||
// without label selector should return 1 result
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
|
||||
// with label selector should return 1 result
|
||||
config = common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=gateway",
|
||||
}
|
||||
analysisResults, err = analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
|
||||
// with wrong label selector should return 0 result
|
||||
config = common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=wrong",
|
||||
}
|
||||
analysisResults, err = analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 0)
|
||||
|
||||
}
|
||||
|
||||
@@ -35,8 +35,13 @@ func (GatewayClassAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
|
||||
|
||||
gcList := >wapi.GatewayClassList{}
|
||||
client := a.Client.CtrlClient
|
||||
gtwapi.AddToScheme(client.Scheme())
|
||||
if err := client.List(a.Context, gcList, &ctrl.ListOptions{}); err != nil {
|
||||
err := gtwapi.AddToScheme(client.Scheme())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labelSelector := util.LabelStrToSelector(a.LabelSelector)
|
||||
if err := client.List(a.Context, gcList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
@@ -29,8 +29,14 @@ func TestGatewayClassAnalyzer(t *testing.T) {
|
||||
GatewayClass.Status.Conditions = []metav1.Condition{BadCondition}
|
||||
// Create a GatewayClassAnalyzer instance with the fake client
|
||||
scheme := scheme.Scheme
|
||||
gtwapi.Install(scheme)
|
||||
apiextensionsv1.AddToScheme(scheme)
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(GatewayClass).Build()
|
||||
|
||||
@@ -49,3 +55,51 @@ func TestGatewayClassAnalyzer(t *testing.T) {
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
|
||||
}
|
||||
|
||||
func TestGatewayClassAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
condition := metav1.Condition{
|
||||
Type: "Accepted",
|
||||
Status: "Ready",
|
||||
Message: "Ready",
|
||||
Reason: "Ready",
|
||||
}
|
||||
|
||||
// Create two GatewayClasses with different labels
|
||||
GatewayClass := >wapi.GatewayClass{}
|
||||
GatewayClass.Name = "foobar"
|
||||
GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller"
|
||||
GatewayClass.Labels = map[string]string{"app": "gatewayclass"}
|
||||
GatewayClass.Status.Conditions = []metav1.Condition{condition}
|
||||
|
||||
GatewayClass2 := >wapi.GatewayClass{}
|
||||
GatewayClass2.Name = "foobar2"
|
||||
GatewayClass2.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller"
|
||||
GatewayClass2.Status.Conditions = []metav1.Condition{condition}
|
||||
|
||||
scheme := scheme.Scheme
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(GatewayClass, GatewayClass2).Build()
|
||||
|
||||
analyzerInstance := GatewayClassAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=gatewayclass",
|
||||
}
|
||||
analysisResults, err := analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
list, err := a.Client.GetClient().AutoscalingV1().HorizontalPodAutoscalers(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().AutoscalingV2().HorizontalPodAutoscalers(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -53,6 +53,17 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
for _, hpa := range list.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
//check the error from status field
|
||||
conditions := hpa.Status.Conditions
|
||||
for _, condition := range conditions {
|
||||
if condition.Status != "True" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: condition.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// check ScaleTargetRef exist
|
||||
scaleTargetRef := hpa.Spec.ScaleTargetRef
|
||||
var podInfo PodInfo
|
||||
@@ -140,8 +151,10 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.HorizontalPodAutoscalers.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.HorizontalPodAutoscalers.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/magiconair/properties/assert"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
|
||||
func TestHPAAnalyzer(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
@@ -55,14 +55,14 @@ func TestHPAAnalyzer(t *testing.T) {
|
||||
|
||||
func TestHPAAnalyzerWithMultipleHPA(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-2",
|
||||
Namespace: "default",
|
||||
@@ -88,14 +88,14 @@ func TestHPAAnalyzerWithMultipleHPA(t *testing.T) {
|
||||
func TestHPAAnalyzerWithUnsuportedScaleTargetRef(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "unsupported",
|
||||
},
|
||||
},
|
||||
@@ -134,14 +134,14 @@ func TestHPAAnalyzerWithUnsuportedScaleTargetRef(t *testing.T) {
|
||||
func TestHPAAnalyzerWithNonExistentScaleTargetRef(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "non-existent",
|
||||
},
|
||||
@@ -181,14 +181,14 @@ func TestHPAAnalyzerWithNonExistentScaleTargetRef(t *testing.T) {
|
||||
func TestHPAAnalyzerWithExistingScaleTargetRefAsDeployment(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "example",
|
||||
},
|
||||
@@ -245,14 +245,14 @@ func TestHPAAnalyzerWithExistingScaleTargetRefAsDeployment(t *testing.T) {
|
||||
func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicationController(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "ReplicationController",
|
||||
Name: "example",
|
||||
},
|
||||
@@ -309,14 +309,14 @@ func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicationController(t *testing
|
||||
func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicaSet(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "ReplicaSet",
|
||||
Name: "example",
|
||||
},
|
||||
@@ -373,14 +373,14 @@ func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicaSet(t *testing.T) {
|
||||
func TestHPAAnalyzerWithExistingScaleTargetRefAsStatefulSet(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "StatefulSet",
|
||||
Name: "example",
|
||||
},
|
||||
@@ -437,14 +437,14 @@ func TestHPAAnalyzerWithExistingScaleTargetRefAsStatefulSet(t *testing.T) {
|
||||
func TestHPAAnalyzerWithExistingScaleTargetRefWithoutSpecifyingResources(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "example",
|
||||
},
|
||||
@@ -503,14 +503,14 @@ func TestHPAAnalyzerWithExistingScaleTargetRefWithoutSpecifyingResources(t *test
|
||||
|
||||
func TestHPAAnalyzerNamespaceFiltering(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
&autoscalingv1.HorizontalPodAutoscaler{
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "other-namespace",
|
||||
@@ -531,3 +531,207 @@ func TestHPAAnalyzerNamespaceFiltering(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
func TestHPAAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "hpa",
|
||||
},
|
||||
},
|
||||
},
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
hpaAnalyzer := HpaAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=hpa",
|
||||
}
|
||||
analysisResults, err := hpaAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
func TestHPAAnalyzerStatusFieldAbleToScale(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "example",
|
||||
},
|
||||
},
|
||||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
|
||||
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
|
||||
{
|
||||
Type: "AbleToScale",
|
||||
Status: "False",
|
||||
Message: "test reason",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
hpaAnalyzer := HpaAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := hpaAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
|
||||
}
|
||||
|
||||
func TestHPAAnalyzerStatusFieldScalingActive(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "example",
|
||||
},
|
||||
},
|
||||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
|
||||
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
|
||||
{
|
||||
Type: autoscalingv2.ScalingActive,
|
||||
Status: "False",
|
||||
Message: "test reason",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
hpaAnalyzer := HpaAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := hpaAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
|
||||
}
|
||||
|
||||
func TestHPAAnalyzerStatusFieldScalingLimited(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "example",
|
||||
},
|
||||
},
|
||||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
|
||||
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
|
||||
{
|
||||
Type: autoscalingv2.ScalingLimited,
|
||||
Status: "False",
|
||||
Message: "test reason",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
hpaAnalyzer := HpaAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := hpaAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
|
||||
}
|
||||
|
||||
func TestHPAAnalyzerStatusField(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "example",
|
||||
},
|
||||
},
|
||||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
|
||||
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
|
||||
{
|
||||
Type: autoscalingv2.AbleToScale,
|
||||
Status: "True",
|
||||
Message: "recommended size matches current size",
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ScalingActive,
|
||||
Status: "True",
|
||||
Message: "the HPA was able to successfully calculate a replica count",
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ScalingLimited,
|
||||
Status: "True",
|
||||
Message: "the desired replica count is less than the minimum replica count",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
hpaAnalyzer := HpaAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := hpaAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
|
||||
}
|
||||
|
||||
@@ -38,14 +38,18 @@ func (HTTPRouteAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
gtw := >wapi.Gateway{}
|
||||
service := &corev1.Service{}
|
||||
client := a.Client.CtrlClient
|
||||
gtwapi.AddToScheme(client.Scheme())
|
||||
if err := client.List(a.Context, routeList, &ctrl.ListOptions{}); err != nil {
|
||||
err := gtwapi.AddToScheme(client.Scheme())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labelSelector := util.LabelStrToSelector(a.LabelSelector)
|
||||
if err := client.List(a.Context, routeList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
// Find all unhealthy gateway Classes
|
||||
|
||||
for _, route := range routeList.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
@@ -104,8 +104,14 @@ func TestGWMissiningHTTRouteAnalyzer(t *testing.T) {
|
||||
HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace)
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
scheme := scheme.Scheme
|
||||
gtwapi.Install(scheme)
|
||||
apiextensionsv1.AddToScheme(scheme)
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
objects := []runtime.Object{
|
||||
&HTTPRoute,
|
||||
}
|
||||
@@ -156,8 +162,14 @@ func TestGWConfigSameHTTRouteAnalyzer(t *testing.T) {
|
||||
Gateway := BuildRouteGateway("differentnamespace", "gatewayname", "Same")
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
scheme := scheme.Scheme
|
||||
gtwapi.Install(scheme)
|
||||
apiextensionsv1.AddToScheme(scheme)
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
objects := []runtime.Object{
|
||||
&HTTPRoute,
|
||||
&Gateway,
|
||||
@@ -207,8 +219,14 @@ func TestGWConfigSelectorHTTRouteAnalyzer(t *testing.T) {
|
||||
Gateway := BuildRouteGateway("default", "gatewayname", "Selector")
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
scheme := scheme.Scheme
|
||||
gtwapi.Install(scheme)
|
||||
apiextensionsv1.AddToScheme(scheme)
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
objects := []runtime.Object{
|
||||
&HTTPRoute,
|
||||
&Gateway,
|
||||
@@ -259,8 +277,14 @@ func TestSvcMissingHTTRouteAnalyzer(t *testing.T) {
|
||||
Gateway := BuildRouteGateway("default", "gatewayname", "Same")
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
scheme := scheme.Scheme
|
||||
gtwapi.Install(scheme)
|
||||
apiextensionsv1.AddToScheme(scheme)
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
objects := []runtime.Object{
|
||||
&HTTPRoute,
|
||||
&Gateway,
|
||||
@@ -332,8 +356,14 @@ func TestSvcDifferentPortHTTRouteAnalyzer(t *testing.T) {
|
||||
Gateway := BuildRouteGateway("default", "gatewayname", "Same")
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
scheme := scheme.Scheme
|
||||
gtwapi.Install(scheme)
|
||||
apiextensionsv1.AddToScheme(scheme)
|
||||
err := gtwapi.Install(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = apiextensionsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
objects := []runtime.Object{
|
||||
&HTTPRoute,
|
||||
&Gateway,
|
||||
@@ -41,7 +41,7 @@ func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
list, err := a.Client.GetClient().NetworkingV1().Ingresses(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().NetworkingV1().Ingresses(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -163,8 +163,10 @@ func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.Ingress.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.Ingress.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,146 +15,226 @@ package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestIngressAnalyzer(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
validIgClassName := new(string)
|
||||
*validIgClassName = "valid-ingress-class"
|
||||
|
||||
var igRule networkingv1.IngressRule
|
||||
|
||||
httpRule := networkingv1.HTTPIngressRuleValue{
|
||||
Paths: []networkingv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/",
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
// This service exists.
|
||||
Name: "Service1",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
ingressAnalyzer := IngressAnalyzer{}
|
||||
{
|
||||
Path: "/test1",
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
// This service is in the test namespace
|
||||
// Hence, it won't be discovered.
|
||||
Name: "Service2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/test2",
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
// This service doesn't exist.
|
||||
Name: "Service3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
igRule.IngressRuleValue.HTTP = &httpRule
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := ingressAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
// Doesn't specify an ingress class.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress2",
|
||||
Namespace: "default",
|
||||
// Specify an invalid ingress class name using annotations.
|
||||
Annotations: map[string]string{
|
||||
"kubernetes.io/ingress.class": "invalid-class",
|
||||
},
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
// Namespace filtering.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress3",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&networkingv1.IngressClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: *validIgClassName,
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress4",
|
||||
Namespace: "default",
|
||||
// Specify valid ingress class name using annotations.
|
||||
Annotations: map[string]string{
|
||||
"kubernetes.io/ingress.class": *validIgClassName,
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// Namespace filtering.
|
||||
Name: "Service2",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Secret1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
&v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Secret2",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress5",
|
||||
Namespace: "default",
|
||||
},
|
||||
|
||||
func TestIngressAnalyzerWithMultipleIngresses(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-2",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
)
|
||||
ingressAnalyzer := IngressAnalyzer{}
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
// Specify valid ingress class name in spec.
|
||||
Spec: networkingv1.IngressSpec{
|
||||
IngressClassName: validIgClassName,
|
||||
Rules: []networkingv1.IngressRule{
|
||||
igRule,
|
||||
},
|
||||
TLS: []networkingv1.IngressTLS{
|
||||
{
|
||||
// This won't contribute to any failures.
|
||||
SecretName: "Secret1",
|
||||
},
|
||||
{
|
||||
// This secret won't be discovered because of namespace filtering.
|
||||
SecretName: "Secret2",
|
||||
},
|
||||
{
|
||||
// This secret doesn't exist.
|
||||
SecretName: "Secret3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
analysisResults, err := ingressAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
igAnalyzer := IngressAnalyzer{}
|
||||
results, err := igAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/Ingress1",
|
||||
failuresCount: 1,
|
||||
},
|
||||
{
|
||||
name: "default/Ingress2",
|
||||
failuresCount: 1,
|
||||
},
|
||||
{
|
||||
name: "default/Ingress5",
|
||||
failuresCount: 4,
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 2)
|
||||
}
|
||||
|
||||
func TestIngressAnalyzerWithoutIngressClassAnnotation(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
})
|
||||
ingressAnalyzer := IngressAnalyzer{}
|
||||
func TestIngressAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
validIgClassName := new(string)
|
||||
*validIgClassName = "valid-ingress-class"
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "ingress",
|
||||
},
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=ingress",
|
||||
}
|
||||
|
||||
analysisResults, err := ingressAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
igAnalyzer := IngressAnalyzer{}
|
||||
results, err := igAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/Ingress1", results[0].Name)
|
||||
|
||||
var errorFound bool
|
||||
for _, analysis := range analysisResults {
|
||||
for _, err := range analysis.Error {
|
||||
if strings.Contains(err.Text, "does not specify an Ingress class") {
|
||||
errorFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if errorFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !errorFound {
|
||||
t.Error("expected error 'does not specify an Ingress class' not found in analysis results")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressAnalyzerNamespaceFiltering(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "other-namespace",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
})
|
||||
ingressAnalyzer := IngressAnalyzer{}
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := ingressAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func (LogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
})
|
||||
|
||||
// search all namespaces for pods that are not running
|
||||
list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -49,29 +49,17 @@ func (LogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
// Iterate through each pod
|
||||
|
||||
for _, pod := range list.Items {
|
||||
var failures []common.Failure
|
||||
podName := pod.Name
|
||||
podLogOptions := v1.PodLogOptions{
|
||||
TailLines: &tailLines,
|
||||
}
|
||||
|
||||
podLogs, err := a.Client.Client.CoreV1().Pods(pod.Namespace).GetLogs(podName, &podLogOptions).DoRaw(a.Context)
|
||||
if err != nil {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Error %s from Pod %s", err.Error(), pod.Name),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: pod.Name,
|
||||
Masked: util.MaskString(pod.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
} else {
|
||||
rawlogs := string(podLogs)
|
||||
if errorPattern.MatchString(rawlogs) {
|
||||
for _, c := range pod.Spec.Containers {
|
||||
var failures []common.Failure
|
||||
podLogOptions := v1.PodLogOptions{
|
||||
TailLines: &tailLines,
|
||||
Container: c.Name,
|
||||
}
|
||||
podLogs, err := a.Client.Client.CoreV1().Pods(pod.Namespace).GetLogs(podName, &podLogOptions).DoRaw(a.Context)
|
||||
if err != nil {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: printErrorLines(pod.Name, pod.Namespace, rawlogs, errorPattern),
|
||||
Text: fmt.Sprintf("Error %s from Pod %s", err.Error(), pod.Name),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: pod.Name,
|
||||
@@ -79,14 +67,27 @@ func (LogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
rawlogs := string(podLogs)
|
||||
if errorPattern.MatchString(strings.ToLower(rawlogs)) {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: printErrorLines(rawlogs, errorPattern),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: pod.Name,
|
||||
Masked: util.MaskString(pod.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{
|
||||
FailureDetails: failures,
|
||||
Pod: pod,
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s/%s", pod.Namespace, pod.Name, c.Name)] = common.PreAnalysis{
|
||||
FailureDetails: failures,
|
||||
Pod: pod,
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, pod.Name, pod.Namespace).Set(float64(len(failures)))
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, pod.Name, pod.Namespace).Set(float64(len(failures)))
|
||||
}
|
||||
}
|
||||
for key, value := range preAnalysis {
|
||||
@@ -95,20 +96,22 @@ func (LogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
parent, _ := util.GetParent(a.Client, value.Pod.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.Pod.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
}
|
||||
func printErrorLines(podName, namespace, logs string, errorPattern *regexp.Regexp) string {
|
||||
func printErrorLines(logs string, errorPattern *regexp.Regexp) string {
|
||||
// Split the logs into lines
|
||||
logLines := strings.Split(logs, "\n")
|
||||
|
||||
// Check each line for errors and print the lines containing errors
|
||||
for _, line := range logLines {
|
||||
if errorPattern.MatchString(line) {
|
||||
if errorPattern.MatchString(strings.ToLower(line)) {
|
||||
return line
|
||||
}
|
||||
}
|
||||
|
||||
173
pkg/analyzer/log_test.go
Normal file
173
pkg/analyzer/log_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestLogAnalyzer(t *testing.T) {
|
||||
oldPattern := errorPattern
|
||||
errorPattern = regexp.MustCompile(`(fake logs)`)
|
||||
t.Cleanup(func() {
|
||||
errorPattern = oldPattern
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"Name": "Pod1",
|
||||
"Namespace": "default",
|
||||
},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "test-container1",
|
||||
},
|
||||
{
|
||||
Name: "test-container2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod2",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"Name": "Pod1",
|
||||
"Namespace": "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod3",
|
||||
Namespace: "test-namespace",
|
||||
Labels: map[string]string{
|
||||
"Name": "Pod1",
|
||||
"Namespace": "test-namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod4",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"Name": "Pod4",
|
||||
"Namespace": "default",
|
||||
},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "test-container3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
logAnalyzer := LogAnalyzer{}
|
||||
results, err := logAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []string{"default/Pod1/test-container1", "default/Pod1/test-container2", "default/Pod4/test-container3"}
|
||||
|
||||
for i, expectation := range expectations {
|
||||
require.Equal(t, expectation, results[i].Name)
|
||||
|
||||
for _, failure := range results[i].Error {
|
||||
require.Equal(t, "fake logs", failure.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
oldPattern := errorPattern
|
||||
errorPattern = regexp.MustCompile(`(fake logs)`)
|
||||
t.Cleanup(func() {
|
||||
errorPattern = oldPattern
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "log",
|
||||
},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "test-container1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "test-container2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=log",
|
||||
}
|
||||
|
||||
logAnalyzer := LogAnalyzer{}
|
||||
results, err := logAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/Pod1/test-container1", results[0].Name)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func (MutatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, erro
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
mutatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), v1.ListOptions{})
|
||||
mutatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), v1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -75,6 +75,11 @@ func (MutatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, erro
|
||||
},
|
||||
},
|
||||
})
|
||||
preAnalysis[fmt.Sprintf("%s/%s", webhookConfig.Namespace, webhook.Name)] = common.PreAnalysis{
|
||||
MutatingWebhook: webhookConfig,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, webhook.Name, webhookConfig.Namespace).Set(float64(len(failures)))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -146,8 +151,10 @@ func (MutatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, erro
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.MutatingWebhook.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.MutatingWebhook.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
|
||||
215
pkg/analyzer/mutating_webhook_test.go
Normal file
215
pkg/analyzer/mutating_webhook_test.go
Normal file
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
Copyright 2024 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/require"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestMutatingWebhookAnalyzer(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"pod": "Pod1",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"pod": "Pod1",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service2",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
// No such pod exists in the test namespace
|
||||
Selector: map[string]string{
|
||||
"pod": "Pod2",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service3",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
// len(service.Spec.Selector) == 0
|
||||
Selector: map[string]string{},
|
||||
},
|
||||
},
|
||||
&admissionregistrationv1.MutatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-mutating-webhook-config",
|
||||
Namespace: "test",
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.MutatingWebhook{
|
||||
{
|
||||
// Failure: Pointing to an inactive receiver pod
|
||||
Name: "webhook1",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Failure: No active pods found in the test namespace
|
||||
Name: "webhook2",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service2",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "webhook3",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service3",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Failure: Service doesn't exist.
|
||||
Name: "webhook4",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service4-doesn't-exist",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Service is nil.
|
||||
Name: "webhook5",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
mwAnalyzer := MutatingWebhookAnalyzer{}
|
||||
results, err := mwAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The results should contain: webhook1, webhook2, and webhook4
|
||||
resultsLen := 3
|
||||
require.Equal(t, resultsLen, len(results))
|
||||
}
|
||||
|
||||
func TestMutatingWebhookAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "mutating-webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "mutating-webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
&admissionregistrationv1.MutatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-mutating-webhook-config",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "mutating-webhook",
|
||||
},
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.MutatingWebhook{
|
||||
{
|
||||
Name: "webhook1",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&admissionregistrationv1.MutatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-mutating-webhook-config2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.MutatingWebhook{
|
||||
{
|
||||
Name: "webhook2",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=mutating-webhook",
|
||||
}
|
||||
|
||||
mwAnalyzer := MutatingWebhookAnalyzer{}
|
||||
results, err := mwAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/webhook1", results[0].Name)
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func (NetworkPolicyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
|
||||
|
||||
// get all network policies in the namespace
|
||||
policies, err := a.Client.GetClient().NetworkingV1().
|
||||
NetworkPolicies(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
NetworkPolicies(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -136,6 +136,19 @@ func TestNetpolWithPod(t *testing.T) {
|
||||
|
||||
func TestNetpolNoPodsNamespaceFiltering(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&networkingv1.NetworkPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "policy-without-podselector-match-labels",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.NetworkPolicySpec{
|
||||
PodSelector: metav1.LabelSelector{
|
||||
// len(MatchLabels) == 0 should trigger a failure.
|
||||
// Allowing traffic to all pods.
|
||||
MatchLabels: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
&networkingv1.NetworkPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
@@ -203,7 +216,50 @@ func TestNetpolNoPodsNamespaceFiltering(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(results), 1)
|
||||
assert.Equal(t, len(results), 2)
|
||||
assert.Equal(t, results[0].Kind, "NetworkPolicy")
|
||||
|
||||
}
|
||||
|
||||
func TestNetpolLabelSelectorFiltering(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&networkingv1.NetworkPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "netpol",
|
||||
},
|
||||
},
|
||||
Spec: networkingv1.NetworkPolicySpec{
|
||||
PodSelector: metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app": "netpol",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&networkingv1.NetworkPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=netpol",
|
||||
}
|
||||
|
||||
analyzer := NetworkPolicyAnalyzer{}
|
||||
results, err := analyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(results), 1)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func (NodeAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
list, err := a.Client.GetClient().CoreV1().Nodes().List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().CoreV1().Nodes().List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,6 +50,9 @@ func (NodeAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
break
|
||||
}
|
||||
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
|
||||
// k3s `EtcdIsVoter`` should not be reported as an error
|
||||
case v1.NodeConditionType("EtcdIsVoter"):
|
||||
break
|
||||
default:
|
||||
if nodeCondition.Status != v1.ConditionFalse {
|
||||
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
|
||||
@@ -74,8 +77,10 @@ func (NodeAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.Node.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.Node.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,110 +15,203 @@ package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestNodeAnalyzerNodeReady(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(&v1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
Type: v1.NodeReady,
|
||||
Status: v1.ConditionTrue,
|
||||
Reason: "KubeletReady",
|
||||
Message: "kubelet is posting ready status",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
func TestNodeAnalyzer(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
}
|
||||
nodeAnalyzer := NodeAnalyzer{}
|
||||
var analysisResults []common.Result
|
||||
analysisResults, err := nodeAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 0)
|
||||
}
|
||||
|
||||
func TestNodeAnalyzerNodeDiskPressure(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(&v1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
Type: v1.NodeDiskPressure,
|
||||
Status: v1.ConditionTrue,
|
||||
Reason: "KubeletHasDiskPressure",
|
||||
Message: "kubelet has disk pressure",
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Node{
|
||||
// A node without Status Conditions shouldn't contribute to failures.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Node1",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Node{
|
||||
// Nodes are not filtered using namespace.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Node2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
// Won't contribute to failures.
|
||||
Type: v1.NodeReady,
|
||||
Status: v1.ConditionTrue,
|
||||
},
|
||||
{
|
||||
// Will contribute to failures.
|
||||
Type: v1.NodeReady,
|
||||
Status: v1.ConditionFalse,
|
||||
},
|
||||
{
|
||||
// Will contribute to failures.
|
||||
Type: v1.NodeReady,
|
||||
Status: v1.ConditionUnknown,
|
||||
},
|
||||
// Non-false statuses for the default cases contribute to failures.
|
||||
{
|
||||
Type: v1.NodeMemoryPressure,
|
||||
Status: v1.ConditionTrue,
|
||||
},
|
||||
{
|
||||
Type: v1.NodeDiskPressure,
|
||||
Status: v1.ConditionTrue,
|
||||
},
|
||||
{
|
||||
Type: v1.NodePIDPressure,
|
||||
Status: v1.ConditionTrue,
|
||||
},
|
||||
{
|
||||
Type: v1.NodeNetworkUnavailable,
|
||||
Status: v1.ConditionTrue,
|
||||
},
|
||||
{
|
||||
Type: v1.NodeMemoryPressure,
|
||||
Status: v1.ConditionUnknown,
|
||||
},
|
||||
{
|
||||
Type: v1.NodeDiskPressure,
|
||||
Status: v1.ConditionUnknown,
|
||||
},
|
||||
{
|
||||
Type: v1.NodePIDPressure,
|
||||
Status: v1.ConditionUnknown,
|
||||
},
|
||||
{
|
||||
Type: v1.NodeNetworkUnavailable,
|
||||
Status: v1.ConditionUnknown,
|
||||
},
|
||||
// A cloud provider may set their own condition and/or a new status
|
||||
// might be introduced. In such cases a failure is assumed and
|
||||
// the code shouldn't break, although it might be a false positive.
|
||||
{
|
||||
Type: "UnknownNodeConditionType",
|
||||
Status: "CompletelyUnknown",
|
||||
},
|
||||
// These won't contribute to failures.
|
||||
{
|
||||
Type: v1.NodeMemoryPressure,
|
||||
Status: v1.ConditionFalse,
|
||||
},
|
||||
{
|
||||
Type: v1.NodeDiskPressure,
|
||||
Status: v1.ConditionFalse,
|
||||
},
|
||||
{
|
||||
Type: v1.NodePIDPressure,
|
||||
Status: v1.ConditionFalse,
|
||||
},
|
||||
{
|
||||
Type: v1.NodeNetworkUnavailable,
|
||||
Status: v1.ConditionFalse,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Node{
|
||||
// A node without any failures shouldn't be present in the results.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Node3",
|
||||
Namespace: "test",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
// Won't contribute to failures.
|
||||
Type: v1.NodeReady,
|
||||
Status: v1.ConditionTrue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "test",
|
||||
}
|
||||
|
||||
nAnalyzer := NodeAnalyzer{}
|
||||
results, err := nAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
expectations := []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "Node2",
|
||||
failuresCount: 11,
|
||||
},
|
||||
Context: context.Background(),
|
||||
}
|
||||
nodeAnalyzer := NodeAnalyzer{}
|
||||
var analysisResults []common.Result
|
||||
analysisResults, err := nodeAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
// A cloud provider may set their own condition and/or a new status might be introduced
|
||||
// In such cases a failure is assumed and the code shouldn't break, although it might be a false positive
|
||||
func TestNodeAnalyzerNodeUnknownType(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(&v1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
Type: "UnknownNodeConditionType",
|
||||
Status: "CompletelyUnknown",
|
||||
Reason: "KubeletHasTheUnknown",
|
||||
Message: "kubelet has the unknown",
|
||||
func TestNodeAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(&v1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Node1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "node",
|
||||
},
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
Type: v1.NodeReady,
|
||||
Status: v1.ConditionFalse,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Node{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Node2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Conditions: []v1.NodeCondition{
|
||||
{
|
||||
Type: v1.NodeReady,
|
||||
Status: v1.ConditionFalse,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
})
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=node",
|
||||
}
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
}
|
||||
nodeAnalyzer := NodeAnalyzer{}
|
||||
var analysisResults []common.Result
|
||||
analysisResults, err := nodeAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
nAnalyzer := NodeAnalyzer{}
|
||||
results, err := nAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "Node1", results[0].Name)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
list, err := a.Client.GetClient().PolicyV1().PodDisruptionBudgets(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().PolicyV1().PodDisruptionBudgets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,6 +50,11 @@ func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
for _, pdb := range list.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
// Before accessing the Conditions, check if they exist or not.
|
||||
if len(pdb.Status.Conditions) == 0 {
|
||||
continue
|
||||
}
|
||||
if pdb.Status.Conditions[0].Type == "DisruptionAllowed" && pdb.Status.Conditions[0].Status == "False" {
|
||||
var doc string
|
||||
if pdb.Spec.MaxUnavailable != nil {
|
||||
@@ -94,8 +99,10 @@ func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.PodDisruptionBudget.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.PodDisruptionBudget.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
|
||||
208
pkg/analyzer/pdb_test.go
Normal file
208
pkg/analyzer/pdb_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
Copyright 2024 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/require"
|
||||
policyv1 "k8s.io/api/policy/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestPodDisruptionBudgetAnalyzer(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&policyv1.PodDisruptionBudget{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PDB1",
|
||||
Namespace: "test",
|
||||
},
|
||||
// Status conditions are nil.
|
||||
Status: policyv1.PodDisruptionBudgetStatus{
|
||||
Conditions: nil,
|
||||
},
|
||||
},
|
||||
&policyv1.PodDisruptionBudget{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PDB2",
|
||||
Namespace: "test",
|
||||
},
|
||||
// Status conditions are empty.
|
||||
Status: policyv1.PodDisruptionBudgetStatus{
|
||||
Conditions: []metav1.Condition{},
|
||||
},
|
||||
},
|
||||
&policyv1.PodDisruptionBudget{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PDB3",
|
||||
Namespace: "test",
|
||||
},
|
||||
Status: policyv1.PodDisruptionBudgetStatus{
|
||||
Conditions: []metav1.Condition{
|
||||
{
|
||||
Type: "DisruptionAllowed",
|
||||
Status: "False",
|
||||
Reason: "test reason",
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: policyv1.PodDisruptionBudgetSpec{
|
||||
MaxUnavailable: &intstr.IntOrString{
|
||||
Type: 0,
|
||||
IntVal: 17,
|
||||
StrVal: "17",
|
||||
},
|
||||
MinAvailable: &intstr.IntOrString{
|
||||
Type: 0,
|
||||
IntVal: 7,
|
||||
StrVal: "7",
|
||||
},
|
||||
// MatchLabels specified.
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"label1": "test1",
|
||||
"label2": "test2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&policyv1.PodDisruptionBudget{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PDB4",
|
||||
Namespace: "test",
|
||||
},
|
||||
Status: policyv1.PodDisruptionBudgetStatus{
|
||||
Conditions: []metav1.Condition{
|
||||
{
|
||||
Type: "DisruptionAllowed",
|
||||
Status: "False",
|
||||
Reason: "test reason",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Match Labels Empty.
|
||||
Spec: policyv1.PodDisruptionBudgetSpec{
|
||||
Selector: &metav1.LabelSelector{},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "test",
|
||||
}
|
||||
|
||||
pdbAnalyzer := PdbAnalyzer{}
|
||||
results, err := pdbAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "test/PDB3", results[0].Name)
|
||||
}
|
||||
|
||||
func TestPodDisruptionBudgetAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&policyv1.PodDisruptionBudget{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PDB1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "pdb",
|
||||
},
|
||||
},
|
||||
// Status conditions are nil.
|
||||
Status: policyv1.PodDisruptionBudgetStatus{
|
||||
Conditions: []metav1.Condition{
|
||||
{
|
||||
Type: "DisruptionAllowed",
|
||||
Status: "False",
|
||||
Reason: "test reason",
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: policyv1.PodDisruptionBudgetSpec{
|
||||
MaxUnavailable: &intstr.IntOrString{
|
||||
Type: 0,
|
||||
IntVal: 17,
|
||||
StrVal: "17",
|
||||
},
|
||||
MinAvailable: &intstr.IntOrString{
|
||||
Type: 0,
|
||||
IntVal: 7,
|
||||
StrVal: "7",
|
||||
},
|
||||
// MatchLabels specified.
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"label1": "test1",
|
||||
"label2": "test2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&policyv1.PodDisruptionBudget{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PDB2",
|
||||
Namespace: "default",
|
||||
},
|
||||
// Status conditions are empty.
|
||||
Status: policyv1.PodDisruptionBudgetStatus{
|
||||
Conditions: []metav1.Condition{
|
||||
{
|
||||
Type: "DisruptionAllowed",
|
||||
Status: "False",
|
||||
Reason: "test reason",
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: policyv1.PodDisruptionBudgetSpec{
|
||||
MaxUnavailable: &intstr.IntOrString{
|
||||
Type: 0,
|
||||
IntVal: 17,
|
||||
StrVal: "17",
|
||||
},
|
||||
MinAvailable: &intstr.IntOrString{
|
||||
Type: 0,
|
||||
IntVal: 7,
|
||||
StrVal: "7",
|
||||
},
|
||||
// MatchLabels specified.
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"label1": "test1",
|
||||
"label2": "test2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=pdb",
|
||||
}
|
||||
|
||||
pdbAnalyzer := PdbAnalyzer{}
|
||||
results, err := pdbAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/PDB1", results[0].Name)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -33,7 +34,9 @@ func (PodAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
})
|
||||
|
||||
// search all namespaces for pods that are not running
|
||||
list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{
|
||||
LabelSelector: a.LabelSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -41,12 +44,12 @@ func (PodAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
for _, pod := range list.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for pending pods
|
||||
if pod.Status.Phase == "Pending" {
|
||||
|
||||
// Check through container status to check for crashes
|
||||
for _, containerStatus := range pod.Status.Conditions {
|
||||
if containerStatus.Type == "PodScheduled" && containerStatus.Reason == "Unschedulable" {
|
||||
if containerStatus.Type == v1.PodScheduled && containerStatus.Reason == "Unschedulable" {
|
||||
if containerStatus.Message != "" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: containerStatus.Message,
|
||||
@@ -57,52 +60,12 @@ func (PodAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check through container status to check for crashes or unready
|
||||
for _, containerStatus := range pod.Status.ContainerStatuses {
|
||||
// Check for errors in the init containers.
|
||||
failures = append(failures, analyzeContainerStatusFailures(a, pod.Status.InitContainerStatuses, pod.Name, pod.Namespace, string(pod.Status.Phase))...)
|
||||
|
||||
if containerStatus.State.Waiting != nil {
|
||||
// Check for errors in containers.
|
||||
failures = append(failures, analyzeContainerStatusFailures(a, pod.Status.ContainerStatuses, pod.Name, pod.Namespace, string(pod.Status.Phase))...)
|
||||
|
||||
if isErrorReason(containerStatus.State.Waiting.Reason) && containerStatus.State.Waiting.Message != "" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: containerStatus.State.Waiting.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
|
||||
// This represents a container that is still being created or blocked due to conditions such as OOMKilled
|
||||
if containerStatus.State.Waiting.Reason == "ContainerCreating" && pod.Status.Phase == "Pending" {
|
||||
|
||||
// parse the event log and append details
|
||||
evt, err := FetchLatestEvent(a.Context, a.Client, pod.Namespace, pod.Name)
|
||||
if err != nil || evt == nil {
|
||||
continue
|
||||
}
|
||||
if evt.Reason == "FailedCreatePodSandBox" && evt.Message != "" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: evt.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// when pod is Running but its ReadinessProbe fails
|
||||
if !containerStatus.Ready && pod.Status.Phase == "Running" {
|
||||
// parse the event log and append details
|
||||
evt, err := FetchLatestEvent(a.Context, a.Client, pod.Namespace, pod.Name)
|
||||
if err != nil || evt == nil {
|
||||
continue
|
||||
}
|
||||
if evt.Reason == "Unhealthy" && evt.Message != "" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: evt.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{
|
||||
Pod: pod,
|
||||
@@ -119,17 +82,85 @@ func (PodAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.Pod.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.Pod.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
}
|
||||
|
||||
func analyzeContainerStatusFailures(a common.Analyzer, statuses []v1.ContainerStatus, name string, namespace string, statusPhase string) []common.Failure {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check through container status to check for crashes or unready
|
||||
for _, containerStatus := range statuses {
|
||||
if containerStatus.State.Waiting != nil {
|
||||
if containerStatus.State.Waiting.Reason == "ContainerCreating" && statusPhase == "Pending" {
|
||||
// This represents a container that is still being created or blocked due to conditions such as OOMKilled
|
||||
// parse the event log and append details
|
||||
evt, err := util.FetchLatestEvent(a.Context, a.Client, namespace, name)
|
||||
if err != nil || evt == nil {
|
||||
continue
|
||||
}
|
||||
if isEvtErrorReason(evt.Reason) && evt.Message != "" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: evt.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
} else if containerStatus.State.Waiting.Reason == "CrashLoopBackOff" && containerStatus.LastTerminationState.Terminated != nil {
|
||||
// This represents container that is in CrashLoopBackOff state due to conditions such as OOMKilled
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("the last termination reason is %s container=%s pod=%s", containerStatus.LastTerminationState.Terminated.Reason, containerStatus.Name, name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
} else if isErrorReason(containerStatus.State.Waiting.Reason) && containerStatus.State.Waiting.Message != "" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: containerStatus.State.Waiting.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// when pod is Running but its ReadinessProbe fails
|
||||
if !containerStatus.Ready && statusPhase == "Running" {
|
||||
// parse the event log and append details
|
||||
evt, err := util.FetchLatestEvent(a.Context, a.Client, namespace, name)
|
||||
if err != nil || evt == nil {
|
||||
continue
|
||||
}
|
||||
if evt.Reason == "Unhealthy" && evt.Message != "" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: evt.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failures
|
||||
}
|
||||
|
||||
func isErrorReason(reason string) bool {
|
||||
failureReasons := []string{
|
||||
"CrashLoopBackOff", "ImagePullBackOff", "CreateContainerConfigError", "PreCreateHookError", "CreateContainerError", "PreStartHookError", "RunContainerError", "ImageInspectError", "ErrImagePull", "ErrImageNeverPull", "InvalidImageName",
|
||||
"CrashLoopBackOff", "ImagePullBackOff", "CreateContainerConfigError", "PreCreateHookError", "CreateContainerError",
|
||||
"PreStartHookError", "RunContainerError", "ImageInspectError", "ErrImagePull", "ErrImageNeverPull", "InvalidImageName",
|
||||
}
|
||||
|
||||
for _, r := range failureReasons {
|
||||
if r == reason {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEvtErrorReason(reason string) bool {
|
||||
failureReasons := []string{
|
||||
"FailedCreatePodSandBox", "FailedMount",
|
||||
}
|
||||
|
||||
for _, r := range failureReasons {
|
||||
|
||||
@@ -15,144 +15,357 @@ package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestPodAnalyzer(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
Conditions: []v1.PodCondition{
|
||||
{
|
||||
Type: v1.PodScheduled,
|
||||
Reason: "Unschedulable",
|
||||
Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.",
|
||||
},
|
||||
tests := []struct {
|
||||
name string
|
||||
config common.Analyzer
|
||||
expectations []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "Pending pods, namespace filtering and readiness probe failure",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
Conditions: []v1.PodCondition{
|
||||
{
|
||||
// This condition will contribute to failures.
|
||||
Type: v1.PodScheduled,
|
||||
Reason: "Unschedulable",
|
||||
Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.",
|
||||
},
|
||||
{
|
||||
// This condition won't contribute to failures.
|
||||
Type: v1.PodScheduled,
|
||||
Reason: "Unexpected failure",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Pod{
|
||||
// This pod won't be selected because of namespace filtering.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod2",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod3",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
// When pod is Running but its ReadinessProbe fails
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
Ready: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Event1",
|
||||
Namespace: "default",
|
||||
},
|
||||
InvolvedObject: v1.ObjectReference{
|
||||
Kind: "Pod",
|
||||
Name: "Pod3",
|
||||
Namespace: "default",
|
||||
},
|
||||
Reason: "Unhealthy",
|
||||
Message: "readiness probe failed: the detail reason here ...",
|
||||
Source: v1.EventSource{Component: "eventTest"},
|
||||
Count: 1,
|
||||
Type: v1.EventTypeWarning,
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example2",
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
Name: "example2",
|
||||
Ready: false,
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/Pod1",
|
||||
failuresCount: 1,
|
||||
},
|
||||
Conditions: []v1.PodCondition{
|
||||
{
|
||||
Type: v1.ContainersReady,
|
||||
Reason: "ContainersNotReady",
|
||||
Message: "containers with unready status: [example2]",
|
||||
},
|
||||
{
|
||||
name: "default/Pod3",
|
||||
failuresCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
// simulate event: 30s Warning Unhealthy pod/my-nginx-7fb4dbcf47-4ch4w Readiness probe failed: bash: xxxx: command not found
|
||||
&v1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
{
|
||||
name: "readiness probe failure without any event",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
// When pod is Running but its ReadinessProbe fails
|
||||
// It won't contribute to any failures because
|
||||
// there's no event present.
|
||||
Phase: v1.PodRunning,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
Ready: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
InvolvedObject: v1.ObjectReference{
|
||||
Kind: "Pod",
|
||||
Name: "example2",
|
||||
Namespace: "default",
|
||||
UID: "differentUid",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Reason: "Unhealthy",
|
||||
Message: "readiness probe failed: the detail reason here ...",
|
||||
Source: v1.EventSource{Component: "eventTest"},
|
||||
Count: 1,
|
||||
Type: v1.EventTypeWarning,
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
podAnalyzer := PodAnalyzer{}
|
||||
var analysisResults []common.Result
|
||||
analysisResults, err := podAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 2)
|
||||
}
|
||||
|
||||
func TestPodAnalyzerNamespaceFiltering(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
{
|
||||
name: "Init container status state waiting",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
InitContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
Ready: true,
|
||||
State: v1.ContainerState{
|
||||
Running: &v1.ContainerStateRunning{
|
||||
StartedAt: metav1.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Ready: false,
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
// This represents a container that is still being created or blocked due to conditions such as OOMKilled
|
||||
Reason: "ContainerCreating",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Event1",
|
||||
Namespace: "default",
|
||||
},
|
||||
InvolvedObject: v1.ObjectReference{
|
||||
Kind: "Pod",
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Reason: "FailedCreatePodSandBox",
|
||||
Message: "failed to create the pod sandbox ...",
|
||||
Type: v1.EventTypeWarning,
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
Conditions: []v1.PodCondition{
|
||||
{
|
||||
Type: v1.PodScheduled,
|
||||
Reason: "Unschedulable",
|
||||
Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/Pod1",
|
||||
failuresCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "other-namespace",
|
||||
Annotations: map[string]string{},
|
||||
{
|
||||
name: "Container status state waiting but no event reported",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
Ready: false,
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
// This represents a container that is still being created or blocked due to conditions such as OOMKilled
|
||||
Reason: "ContainerCreating",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
Conditions: []v1.PodCondition{
|
||||
{
|
||||
Type: v1.PodScheduled,
|
||||
Reason: "Unschedulable",
|
||||
Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Container status state waiting",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
Name: "Container1",
|
||||
Ready: false,
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
// This represents a container that is still being created or blocked due to conditions such as OOMKilled
|
||||
Reason: "ContainerCreating",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Container2",
|
||||
Ready: false,
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
// This represents container that is in CrashLoopBackOff state due to conditions such as OOMKilled
|
||||
Reason: "CrashLoopBackOff",
|
||||
},
|
||||
},
|
||||
LastTerminationState: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{
|
||||
Reason: "test reason",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Container3",
|
||||
Ready: false,
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
// This won't contribute to failures.
|
||||
Reason: "RandomReason",
|
||||
Message: "This container won't be present in the failures",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Container4",
|
||||
Ready: false,
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
// Valid error reason.
|
||||
Reason: "PreStartHookError",
|
||||
Message: "Container4 encountered PreStartHookError",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Container5",
|
||||
Ready: false,
|
||||
State: v1.ContainerState{
|
||||
Waiting: &v1.ContainerStateWaiting{
|
||||
// Valid error reason.
|
||||
Reason: "CrashLoopBackOff",
|
||||
Message: "Container4 encountered CrashLoopBackOff",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Event1",
|
||||
Namespace: "default",
|
||||
},
|
||||
InvolvedObject: v1.ObjectReference{
|
||||
Kind: "Pod",
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
},
|
||||
// This reason won't contribute to failures.
|
||||
Reason: "RandomEvent",
|
||||
Type: v1.EventTypeWarning,
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/Pod1",
|
||||
failuresCount: 3,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
podAnalyzer := PodAnalyzer{}
|
||||
var analysisResults []common.Result
|
||||
analysisResults, err := podAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
results, err := podAnalyzer.Analyze(tt.config)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.expectations == nil {
|
||||
require.Equal(t, 0, len(results))
|
||||
} else {
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
require.Equal(t, len(tt.expectations), len(results))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, tt.expectations[i].name, result.Name)
|
||||
require.Equal(t, tt.expectations[i].failuresCount, len(result.Error))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
appsv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -32,7 +33,7 @@ func (PvcAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
})
|
||||
|
||||
// search all namespaces for pods that are not running
|
||||
list, err := a.Client.GetClient().CoreV1().PersistentVolumeClaims(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().CoreV1().PersistentVolumeClaims(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -43,10 +44,10 @@ func (PvcAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for empty rs
|
||||
if pvc.Status.Phase == "Pending" {
|
||||
if pvc.Status.Phase == appsv1.ClaimPending {
|
||||
|
||||
// parse the event log and append details
|
||||
evt, err := FetchLatestEvent(a.Context, a.Client, pvc.Namespace, pvc.Name)
|
||||
evt, err := util.FetchLatestEvent(a.Context, a.Client, pvc.Namespace, pvc.Name)
|
||||
if err != nil || evt == nil {
|
||||
continue
|
||||
}
|
||||
@@ -73,8 +74,10 @@ func (PvcAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.PersistentVolumeClaim.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.PersistentVolumeClaim.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
|
||||
280
pkg/analyzer/pvc_test.go
Normal file
280
pkg/analyzer/pvc_test.go
Normal file
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
Copyright 2024 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/require"
|
||||
appsv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestPersistentVolumeClaimAnalyzer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config common.Analyzer
|
||||
expectations []string
|
||||
}{
|
||||
{
|
||||
name: "PV1 and PVC5 report failures",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&appsv1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Event1",
|
||||
Namespace: "default",
|
||||
},
|
||||
LastTimestamp: metav1.Time{
|
||||
Time: time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC),
|
||||
},
|
||||
Reason: "ProvisioningFailed",
|
||||
Message: "PVC Event1 provisioning failed",
|
||||
},
|
||||
&appsv1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// This event won't get selected.
|
||||
Name: "Event2",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&appsv1.Event{
|
||||
// This is the latest event.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Event3",
|
||||
Namespace: "default",
|
||||
},
|
||||
LastTimestamp: metav1.Time{
|
||||
Time: time.Date(2024, 4, 15, 10, 0, 0, 0, time.UTC),
|
||||
},
|
||||
Reason: "ProvisioningFailed",
|
||||
Message: "PVC Event3 provisioning failed",
|
||||
},
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
Phase: appsv1.ClaimPending,
|
||||
},
|
||||
},
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
// Won't contribute to failures.
|
||||
Phase: appsv1.ClaimBound,
|
||||
},
|
||||
},
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC3",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
// Won't contribute to failures.
|
||||
Phase: appsv1.ClaimLost,
|
||||
},
|
||||
},
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
// PVCs in namespace other than "default" won't be discovered.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC4",
|
||||
Namespace: "test",
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
Phase: appsv1.ClaimLost,
|
||||
},
|
||||
},
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
// PVCs in namespace other than "default" won't be discovered.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC5",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
Phase: appsv1.ClaimPending,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []string{
|
||||
"default/PVC1",
|
||||
"default/PVC5",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no event",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
Phase: appsv1.ClaimPending,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event other than provision failure",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&appsv1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Event1",
|
||||
Namespace: "default",
|
||||
},
|
||||
// Any reason other than ProvisioningFailed won't result in failure.
|
||||
Reason: "UnknownReason",
|
||||
},
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
Phase: appsv1.ClaimPending,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event without error message",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&appsv1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Event1",
|
||||
Namespace: "default",
|
||||
},
|
||||
// Event without any error message won't result in failure.
|
||||
Reason: "ProvisioningFailed",
|
||||
},
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
Phase: appsv1.ClaimPending,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pvcAnalyzer := PvcAnalyzer{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
results, err := pvcAnalyzer.Analyze(tt.config)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.expectations == nil {
|
||||
require.Equal(t, 0, len(results))
|
||||
} else {
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
require.Equal(t, len(tt.expectations), len(results))
|
||||
|
||||
for i, expectation := range tt.expectations {
|
||||
require.Equal(t, expectation, results[i].Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPvcAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&appsv1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Event1",
|
||||
Namespace: "default",
|
||||
},
|
||||
LastTimestamp: metav1.Time{
|
||||
Time: time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC),
|
||||
},
|
||||
Reason: "ProvisioningFailed",
|
||||
Message: "PVC Event1 provisioning failed",
|
||||
},
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "pvc",
|
||||
},
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
Phase: appsv1.ClaimPending,
|
||||
},
|
||||
},
|
||||
&appsv1.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "PVC2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.PersistentVolumeClaimStatus{
|
||||
Phase: appsv1.ClaimPending,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=pvc",
|
||||
}
|
||||
|
||||
pvcAnalyzer := PvcAnalyzer{}
|
||||
results, err := pvcAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/PVC1", results[0].Name)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func (ReplicaSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
})
|
||||
|
||||
// search all namespaces for pods that are not running
|
||||
list, err := a.Client.GetClient().AppsV1().ReplicaSets(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().AppsV1().ReplicaSets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,8 +72,10 @@ func (ReplicaSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.ReplicaSet.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.ReplicaSet.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
return a.Results, nil
|
||||
|
||||
201
pkg/analyzer/rs_test.go
Normal file
201
pkg/analyzer/rs_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
Copyright 2024 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/require"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestReplicaSetAnalyzer(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ReplicaSet1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.ReplicaSetStatus{
|
||||
Replicas: 0,
|
||||
Conditions: []appsv1.ReplicaSetCondition{
|
||||
{
|
||||
// Should contribute to failures.
|
||||
Type: appsv1.ReplicaSetReplicaFailure,
|
||||
Reason: "FailedCreate",
|
||||
Message: "failed to create test replica set 1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&appsv1.ReplicaSet{
|
||||
// This replicaset won't be discovered as it is not in the
|
||||
// default namespace.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ReplicaSet2",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ReplicaSet3",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.ReplicaSetStatus{
|
||||
Replicas: 0,
|
||||
Conditions: []appsv1.ReplicaSetCondition{
|
||||
{
|
||||
Type: appsv1.ReplicaSetReplicaFailure,
|
||||
// Should not be included in the failures.
|
||||
Reason: "RandomError",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ReplicaSet4",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.ReplicaSetStatus{
|
||||
Replicas: 0,
|
||||
Conditions: []appsv1.ReplicaSetCondition{
|
||||
{
|
||||
// Should contribute to failures.
|
||||
Type: appsv1.ReplicaSetReplicaFailure,
|
||||
Reason: "FailedCreate",
|
||||
Message: "failed to create test replica set 4 condition 1",
|
||||
},
|
||||
{
|
||||
// Should not contribute to failures.
|
||||
Type: appsv1.ReplicaSetReplicaFailure,
|
||||
Reason: "Unknown",
|
||||
},
|
||||
{
|
||||
// Should not contribute to failures.
|
||||
Type: appsv1.ReplicaSetReplicaFailure,
|
||||
Reason: "FailedCreate",
|
||||
Message: "failed to create test replica set 4 condition 3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&appsv1.ReplicaSet{
|
||||
// Replicaset without any failures.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ReplicaSet5",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.ReplicaSetStatus{
|
||||
Replicas: 3,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
rsAnalyzer := ReplicaSetAnalyzer{}
|
||||
results, err := rsAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/ReplicaSet1",
|
||||
failuresCount: 1,
|
||||
},
|
||||
{
|
||||
name: "default/ReplicaSet4",
|
||||
failuresCount: 2,
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplicaSetAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ReplicaSet1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "replicaset",
|
||||
},
|
||||
},
|
||||
Status: appsv1.ReplicaSetStatus{
|
||||
Replicas: 0,
|
||||
Conditions: []appsv1.ReplicaSetCondition{
|
||||
{
|
||||
// Should contribute to failures.
|
||||
Type: appsv1.ReplicaSetReplicaFailure,
|
||||
Reason: "FailedCreate",
|
||||
Message: "failed to create test replica set 1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ReplicaSet2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: appsv1.ReplicaSetStatus{
|
||||
Replicas: 0,
|
||||
Conditions: []appsv1.ReplicaSetCondition{
|
||||
{
|
||||
// Should contribute to failures.
|
||||
Type: appsv1.ReplicaSetReplicaFailure,
|
||||
Reason: "FailedCreate",
|
||||
Message: "failed to create test replica set 1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=replicaset",
|
||||
}
|
||||
|
||||
rsAnalyzer := ReplicaSetAnalyzer{}
|
||||
results, err := rsAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/ReplicaSet1", results[0].Name)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
})
|
||||
|
||||
// search all namespaces for pods that are not running
|
||||
list, err := a.Client.GetClient().CoreV1().Endpoints(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().CoreV1().Endpoints(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,18 +98,35 @@ func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
count++
|
||||
pods = append(pods, addresses.TargetRef.Kind+"/"+addresses.TargetRef.Name)
|
||||
}
|
||||
|
||||
doc := apiDoc.GetApiDocV2("subsets.notReadyAddresses")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Service has not ready endpoints, pods: %s, expected %d", pods, count),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
doc := apiDoc.GetApiDocV2("subsets.notReadyAddresses")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Service has not ready endpoints, pods: %s, expected %d", pods, count),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
// fetch event
|
||||
events, err := a.Client.GetClient().CoreV1().Events(a.Namespace).List(a.Context,
|
||||
metav1.ListOptions{
|
||||
FieldSelector: "involvedObject.name=" + ep.Name,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, event := range events.Items {
|
||||
if event.Type != "Normal" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Service %s/%s has event %s", ep.Namespace, ep.Name, event.Message),
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", ep.Namespace, ep.Name)] = common.PreAnalysis{
|
||||
Endpoint: ep,
|
||||
@@ -126,8 +143,10 @@ func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.Endpoint.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.Endpoint.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
return a.Results, nil
|
||||
|
||||
@@ -15,108 +15,256 @@ package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
)
|
||||
|
||||
func TestServiceAnalyzer(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "example",
|
||||
},
|
||||
}})
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Endpoint1",
|
||||
Namespace: "test",
|
||||
},
|
||||
// Endpoint with non-zero subsets.
|
||||
Subsets: []v1.EndpointSubset{
|
||||
{
|
||||
// These not ready end points will contribute to failures.
|
||||
NotReadyAddresses: []v1.EndpointAddress{
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "test-reference",
|
||||
Name: "reference1",
|
||||
},
|
||||
},
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "test-reference",
|
||||
Name: "reference2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// These not ready end points will contribute to failures.
|
||||
NotReadyAddresses: []v1.EndpointAddress{
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "test-reference",
|
||||
Name: "reference3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Endpoint2",
|
||||
Namespace: "test",
|
||||
Annotations: map[string]string{
|
||||
// Leader election record annotation key defined.
|
||||
resourcelock.LeaderElectionRecordAnnotationKey: "this is okay",
|
||||
},
|
||||
},
|
||||
// Endpoint with zero subsets.
|
||||
},
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// This won't contribute to any failures.
|
||||
Name: "non-existent-service",
|
||||
Namespace: "test",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
// Endpoint with zero subsets.
|
||||
},
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service1",
|
||||
Namespace: "test",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
// Endpoint with zero subsets.
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service1",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app1": "test-app1",
|
||||
"app2": "test-app2",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// This service won't be discovered.
|
||||
Name: "Service2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app1": "test-app1",
|
||||
"app2": "test-app2",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service3",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
// No Spec Selector
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
Namespace: "test",
|
||||
}
|
||||
|
||||
serviceAnalyzer := ServiceAnalyzer{}
|
||||
analysisResults, err := serviceAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
sAnalyzer := ServiceAnalyzer{}
|
||||
results, err := sAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "test/Endpoint1",
|
||||
failuresCount: 1,
|
||||
},
|
||||
{
|
||||
name: "test/Service1",
|
||||
failuresCount: 2,
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
func TestServiceAnalyzerNamespaceFiltering(t *testing.T) {
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "example",
|
||||
func TestServiceAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
clientSet :=
|
||||
fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Endpoint1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "service",
|
||||
"part-of": "test",
|
||||
},
|
||||
},
|
||||
// Endpoint with non-zero subsets.
|
||||
Subsets: []v1.EndpointSubset{
|
||||
{
|
||||
// These not ready end points will contribute to failures.
|
||||
NotReadyAddresses: []v1.EndpointAddress{
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "test-reference",
|
||||
Name: "reference1",
|
||||
},
|
||||
},
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "test-reference",
|
||||
Name: "reference2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// These not ready end points will contribute to failures.
|
||||
NotReadyAddresses: []v1.EndpointAddress{
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "test-reference",
|
||||
Name: "reference3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "other-namespace",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "other-namespace",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "example",
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "service",
|
||||
},
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app1": "test-app1",
|
||||
"app2": "test-app2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app1": "test-app1",
|
||||
"app2": "test-app2",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=service",
|
||||
}
|
||||
|
||||
serviceAnalyzer := ServiceAnalyzer{}
|
||||
analysisResults, err := serviceAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
sAnalyzer := ServiceAnalyzer{}
|
||||
results, err := sAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/Endpoint1", results[0].Name)
|
||||
|
||||
config = common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=service,part-of=test",
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
|
||||
sAnalyzer = ServiceAnalyzer{}
|
||||
results, err = sAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/Endpoint1", results[0].Name)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
@@ -41,7 +42,7 @@ func (StatefulSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
list, err := a.Client.GetClient().AppsV1().StatefulSets(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
list, err := a.Client.GetClient().AppsV1().StatefulSets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -93,6 +94,41 @@ func (StatefulSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if sts.Spec.Replicas != nil && *(sts.Spec.Replicas) != sts.Status.AvailableReplicas {
|
||||
for i := int32(0); i < *(sts.Spec.Replicas); i++ {
|
||||
podName := sts.Name + "-" + fmt.Sprint(i)
|
||||
pod, err := a.Client.GetClient().CoreV1().Pods(sts.Namespace).Get(a.Context, podName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) && i == 0 {
|
||||
evt, err := util.FetchLatestEvent(a.Context, a.Client, sts.Namespace, sts.Name)
|
||||
if err != nil || evt == nil || evt.Type == "Normal" {
|
||||
break
|
||||
}
|
||||
failures = append(failures, common.Failure{
|
||||
Text: evt.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
if pod.Status.Phase != "Running" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Statefulset pod %s in the namespace %s is not in running state.", pod.Name, pod.Namespace),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: sts.Namespace,
|
||||
Masked: util.MaskString(pod.Name),
|
||||
},
|
||||
{
|
||||
Unmasked: serviceName,
|
||||
Masked: util.MaskString(pod.Namespace),
|
||||
},
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", sts.Namespace, sts.Name)] = common.PreAnalysis{
|
||||
StatefulSet: sts,
|
||||
@@ -109,8 +145,10 @@ func (StatefulSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.StatefulSet.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.StatefulSet.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestStatefulSetAnalyzerMissingStorageClass(t *testing.T) {
|
||||
AccessModes: []corev1.PersistentVolumeAccessMode{
|
||||
"ReadWriteOnce",
|
||||
},
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Resources: corev1.VolumeResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
corev1.ResourceStorage: resource.MustParse("1Gi"),
|
||||
},
|
||||
@@ -188,3 +188,217 @@ func TestStatefulSetAnalyzerNamespaceFiltering(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
func TestStatefulSetAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
clientSet := fake.NewSimpleClientset(
|
||||
&appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "statefulset",
|
||||
"part-of": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
&appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=statefulset",
|
||||
}
|
||||
statefulSetAnalyzer := StatefulSetAnalyzer{}
|
||||
results, err := statefulSetAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "default/example1", results[0].Name)
|
||||
|
||||
config = common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=statefulset,part-of=test",
|
||||
}
|
||||
statefulSetAnalyzer = StatefulSetAnalyzer{}
|
||||
results, err = statefulSetAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "default/example1", results[0].Name)
|
||||
}
|
||||
|
||||
func TestStatefulSetAnalyzerReplica(t *testing.T) {
|
||||
replicas := int32(3)
|
||||
pods := []*corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-0",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
},
|
||||
}
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Replicas: &replicas,
|
||||
},
|
||||
Status: appsv1.StatefulSetStatus{
|
||||
AvailableReplicas: 3,
|
||||
},
|
||||
},
|
||||
pods[0], pods[1], pods[2],
|
||||
)
|
||||
statefulSetAnalyzer := StatefulSetAnalyzer{}
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := statefulSetAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
func TestStatefulSetAnalyzerUnavailableReplicas(t *testing.T) {
|
||||
replicas := int32(3)
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Replicas: &replicas,
|
||||
},
|
||||
Status: appsv1.StatefulSetStatus{
|
||||
AvailableReplicas: 0,
|
||||
},
|
||||
})
|
||||
statefulSetAnalyzer := StatefulSetAnalyzer{}
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := statefulSetAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
func TestStatefulSetAnalyzerUnavailableReplicaWithPodInitialized(t *testing.T) {
|
||||
replicas := int32(3)
|
||||
pods := []*corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-0",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodRunning,
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example-1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Phase: corev1.PodPending,
|
||||
},
|
||||
},
|
||||
}
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Replicas: &replicas,
|
||||
},
|
||||
Status: appsv1.StatefulSetStatus{
|
||||
AvailableReplicas: 1,
|
||||
},
|
||||
},
|
||||
pods[0], pods[1],
|
||||
)
|
||||
statefulSetAnalyzer := StatefulSetAnalyzer{}
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := statefulSetAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var errorFound bool
|
||||
want := "Statefulset pod example-1 in the namespace default is not in running state."
|
||||
|
||||
for _, analysis := range analysisResults {
|
||||
for _, got := range analysis.Error {
|
||||
if want == got.Text {
|
||||
errorFound = true
|
||||
}
|
||||
}
|
||||
if errorFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !errorFound {
|
||||
t.Errorf("Error expected: '%v', not found in StatefulSet's analysis results", want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ type ValidatingWebhookAnalyzer struct{}
|
||||
|
||||
func (ValidatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "ValidatingWebhookConfgiguration"
|
||||
kind := "ValidatingWebhookConfiguration"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
@@ -42,7 +42,7 @@ func (ValidatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, er
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
validatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.Background(), v1.ListOptions{})
|
||||
validatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.Background(), v1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -73,6 +73,11 @@ func (ValidatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, er
|
||||
},
|
||||
},
|
||||
})
|
||||
preAnalysis[fmt.Sprintf("%s/%s", webhookConfig.Namespace, webhook.Name)] = common.PreAnalysis{
|
||||
ValidatingWebhook: webhookConfig,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, webhook.Name, webhookConfig.Namespace).Set(float64(len(failures)))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -144,8 +149,10 @@ func (ValidatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, er
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.ValidatingWebhook.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
parent, found := util.GetParent(a.Client, value.ValidatingWebhook.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
|
||||
217
pkg/analyzer/validating_webhook_test.go
Normal file
217
pkg/analyzer/validating_webhook_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
Copyright 2024 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/require"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestValidatingWebhookAnalyzer(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"pod": "Pod1",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"pod": "Pod1",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service2",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
// No such pod exists in the test namespace
|
||||
Selector: map[string]string{
|
||||
"pod": "Pod2",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service3",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
// len(service.Spec.Selector) == 0
|
||||
Selector: map[string]string{},
|
||||
},
|
||||
},
|
||||
&admissionregistrationv1.ValidatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-validating-webhook-config",
|
||||
Namespace: "test",
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{
|
||||
{
|
||||
// Failure: Pointing to an inactive receiver pod
|
||||
Name: "webhook1",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Failure: No active pods found in the test namespace
|
||||
Name: "webhook2",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service2",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "webhook3",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service3",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Failure: Service doesn't exist.
|
||||
Name: "webhook4",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service4-doesn't-exist",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Service is nil.
|
||||
Name: "webhook5",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
vwAnalyzer := ValidatingWebhookAnalyzer{}
|
||||
results, err := vwAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The results should contain: webhook1, webhook2, and webhook4
|
||||
resultsLen := 3
|
||||
require.Equal(t, resultsLen, len(results))
|
||||
}
|
||||
|
||||
func TestValidatingWebhookAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
clientSet := fake.NewSimpleClientset(
|
||||
&admissionregistrationv1.ValidatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-validating-webhook-config1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "validating-webhook",
|
||||
"part-of": "test",
|
||||
},
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{
|
||||
{
|
||||
// Failure: Pointing to an inactive receiver pod
|
||||
Name: "webhook1",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&admissionregistrationv1.ValidatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-validating-webhook-config2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Webhooks: []admissionregistrationv1.ValidatingWebhook{
|
||||
{
|
||||
// Failure: Pointing to an inactive receiver pod
|
||||
Name: "webhook1",
|
||||
ClientConfig: admissionregistrationv1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1.ServiceReference{
|
||||
Name: "test-service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=validating-webhook",
|
||||
}
|
||||
|
||||
vwAnalyzer := ValidatingWebhookAnalyzer{}
|
||||
results, err := vwAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
require.Equal(t, 1, len(results))
|
||||
|
||||
config = common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=validating-webhook,part-of=test",
|
||||
}
|
||||
|
||||
vwAnalyzer = ValidatingWebhookAnalyzer{}
|
||||
results, err = vwAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
require.Equal(t, 1, len(results))
|
||||
}
|
||||
26
pkg/cache/cache.go
vendored
26
pkg/cache/cache.go
vendored
@@ -14,6 +14,7 @@ var (
|
||||
&FileBasedCache{},
|
||||
&GCSCache{},
|
||||
&S3Cache{},
|
||||
&InterplexCache{},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -47,20 +48,28 @@ func ParseCacheConfiguration() (CacheProvider, error) {
|
||||
return cacheInfo, nil
|
||||
}
|
||||
|
||||
func NewCacheProvider(cacheType, bucketname, region, storageAccount, containerName, projectId string) (CacheProvider, error) {
|
||||
func NewCacheProvider(cacheType, bucketname, region, endpoint, storageAccount, containerName, projectId string, insecure bool) (CacheProvider, error) {
|
||||
cProvider := CacheProvider{}
|
||||
|
||||
switch {
|
||||
case cacheType == "azure":
|
||||
cProvider.Azure.ContainerName = containerName
|
||||
cProvider.Azure.StorageAccount = storageAccount
|
||||
cProvider.CurrentCacheType = "azure"
|
||||
case cacheType == "gcs":
|
||||
cProvider.GCS.BucketName = bucketname
|
||||
cProvider.GCS.ProjectId = projectId
|
||||
cProvider.GCS.Region = region
|
||||
cProvider.CurrentCacheType = "gcs"
|
||||
case cacheType == "s3":
|
||||
cProvider.S3.BucketName = bucketname
|
||||
cProvider.S3.Region = region
|
||||
cProvider.S3.Endpoint = endpoint
|
||||
cProvider.S3.InsecureSkipVerify = insecure
|
||||
cProvider.CurrentCacheType = "s3"
|
||||
case cacheType == "interplex":
|
||||
cProvider.Interplex.ConnectionString = endpoint
|
||||
cProvider.CurrentCacheType = "interplex"
|
||||
default:
|
||||
return CacheProvider{}, status.Error(codes.Internal, fmt.Sprintf("%s is not a valid option", cacheType))
|
||||
}
|
||||
@@ -81,21 +90,20 @@ func GetCacheConfiguration() (ICache, error) {
|
||||
}
|
||||
|
||||
var cache ICache
|
||||
|
||||
switch {
|
||||
case cacheInfo.GCS != GCSCacheConfiguration{}:
|
||||
case cacheInfo.CurrentCacheType == "gcs":
|
||||
cache = &GCSCache{}
|
||||
case cacheInfo.Azure != AzureCacheConfiguration{}:
|
||||
case cacheInfo.CurrentCacheType == "azure":
|
||||
cache = &AzureCache{}
|
||||
case cacheInfo.S3 != S3CacheConfiguration{}:
|
||||
case cacheInfo.CurrentCacheType == "s3":
|
||||
cache = &S3Cache{}
|
||||
case cacheInfo.CurrentCacheType == "interplex":
|
||||
cache = &InterplexCache{}
|
||||
default:
|
||||
cache = &FileBasedCache{}
|
||||
}
|
||||
|
||||
cache.Configure(cacheInfo)
|
||||
|
||||
return cache, nil
|
||||
err_config := cache.Configure(cacheInfo)
|
||||
return cache, err_config
|
||||
}
|
||||
|
||||
func AddRemoteCache(cacheInfo CacheProvider) error {
|
||||
|
||||
108
pkg/cache/interplex_based.go
vendored
Normal file
108
pkg/cache/interplex_based.go
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
rpc "buf.build/gen/go/interplex-ai/schemas/grpc/go/protobuf/schema/v1/schemav1grpc"
|
||||
schemav1 "buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go/protobuf/schema/v1"
|
||||
"context"
|
||||
"errors"
|
||||
"google.golang.org/grpc"
|
||||
"os"
|
||||
)
|
||||
|
||||
var _ ICache = (*InterplexCache)(nil)
|
||||
|
||||
type InterplexCache struct {
|
||||
configuration InterplexCacheConfiguration
|
||||
client InterplexClient
|
||||
cacheServiceClient rpc.CacheServiceClient
|
||||
noCache bool
|
||||
}
|
||||
|
||||
type InterplexCacheConfiguration struct {
|
||||
ConnectionString string `mapstructure:"connectionString" yaml:"connectionString,omitempty"`
|
||||
}
|
||||
|
||||
type InterplexClient struct {
|
||||
}
|
||||
|
||||
func (c *InterplexCache) Configure(cacheInfo CacheProvider) error {
|
||||
|
||||
if cacheInfo.Interplex.ConnectionString == "" {
|
||||
return errors.New("connection string is required")
|
||||
}
|
||||
c.configuration.ConnectionString = cacheInfo.Interplex.ConnectionString
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *InterplexCache) Store(key string, data string) error {
|
||||
|
||||
if os.Getenv("INTERPLEX_LOCAL_MODE") != "" {
|
||||
c.configuration.ConnectionString = "localhost:8084"
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(c.configuration.ConnectionString, grpc.WithInsecure(), grpc.WithBlock())
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceClient := rpc.NewCacheServiceClient(conn)
|
||||
c.cacheServiceClient = serviceClient
|
||||
req := schemav1.SetRequest{
|
||||
Key: key,
|
||||
Value: data,
|
||||
}
|
||||
_, err = c.cacheServiceClient.Set(context.Background(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *InterplexCache) Load(key string) (string, error) {
|
||||
conn, err := grpc.NewClient(c.configuration.ConnectionString, grpc.WithInsecure(), grpc.WithBlock())
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
serviceClient := rpc.NewCacheServiceClient(conn)
|
||||
c.cacheServiceClient = serviceClient
|
||||
req := schemav1.GetRequest{
|
||||
Key: key,
|
||||
}
|
||||
resp, err := c.cacheServiceClient.Get(context.Background(), &req)
|
||||
// check if response is cache error not found
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Value, nil
|
||||
}
|
||||
|
||||
func (InterplexCache) List() ([]CacheObjectDetails, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (InterplexCache) Remove(key string) error {
|
||||
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (c *InterplexCache) Exists(key string) bool {
|
||||
if _, err := c.Load(key); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *InterplexCache) IsCacheDisabled() bool {
|
||||
return c.noCache
|
||||
}
|
||||
|
||||
func (InterplexCache) GetName() string {
|
||||
//TODO implement me
|
||||
return "interplex"
|
||||
}
|
||||
|
||||
func (c *InterplexCache) DisableCache() {
|
||||
c.noCache = true
|
||||
}
|
||||
77
pkg/cache/interplex_based_test.go
vendored
Normal file
77
pkg/cache/interplex_based_test.go
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
rpc "buf.build/gen/go/interplex-ai/schemas/grpc/go/protobuf/schema/v1/schemav1grpc"
|
||||
schemav1 "buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go/protobuf/schema/v1"
|
||||
"context"
|
||||
"errors"
|
||||
"google.golang.org/grpc"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInterplexCache(t *testing.T) {
|
||||
cache := &InterplexCache{
|
||||
configuration: InterplexCacheConfiguration{
|
||||
ConnectionString: "localhost:50051",
|
||||
},
|
||||
}
|
||||
|
||||
// Mock GRPC server setup
|
||||
go func() {
|
||||
lis, err := net.Listen("tcp", ":50051")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
s := grpc.NewServer()
|
||||
rpc.RegisterCacheServiceServer(s, &mockCacheService{})
|
||||
if err := s.Serve(lis); err != nil {
|
||||
t.Fatalf("failed to serve: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("TestStore", func(t *testing.T) {
|
||||
err := cache.Store("key1", "value1")
|
||||
if err != nil {
|
||||
t.Errorf("Error storing value: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TestLoad", func(t *testing.T) {
|
||||
value, err := cache.Load("key1")
|
||||
if err != nil {
|
||||
t.Errorf("Error loading value: %v", err)
|
||||
}
|
||||
if value != "value1" {
|
||||
t.Errorf("Expected value1, got %v", value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TestExists", func(t *testing.T) {
|
||||
exists := cache.Exists("key1")
|
||||
if !exists {
|
||||
t.Errorf("Expected key1 to exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type mockCacheService struct {
|
||||
rpc.UnimplementedCacheServiceServer
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (m *mockCacheService) Set(ctx context.Context, req *schemav1.SetRequest) (*schemav1.SetResponse, error) {
|
||||
if m.data == nil {
|
||||
m.data = make(map[string]string)
|
||||
}
|
||||
m.data[req.Key] = req.Value
|
||||
return &schemav1.SetResponse{}, nil
|
||||
}
|
||||
|
||||
func (m *mockCacheService) Get(ctx context.Context, req *schemav1.GetRequest) (*schemav1.GetResponse, error) {
|
||||
value, exists := m.data[req.Key]
|
||||
if !exists {
|
||||
return nil, errors.New("key not found")
|
||||
}
|
||||
return &schemav1.GetResponse{Value: value}, nil
|
||||
}
|
||||
24
pkg/cache/s3_based.go
vendored
24
pkg/cache/s3_based.go
vendored
@@ -2,7 +2,9 @@ package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
@@ -17,17 +19,16 @@ type S3Cache struct {
|
||||
}
|
||||
|
||||
type S3CacheConfiguration struct {
|
||||
Region string `mapstructure:"region" yaml:"region,omitempty"`
|
||||
BucketName string `mapstructure:"bucketname" yaml:"bucketname,omitempty"`
|
||||
Region string `mapstructure:"region" yaml:"region,omitempty"`
|
||||
BucketName string `mapstructure:"bucketname" yaml:"bucketname,omitempty"`
|
||||
Endpoint string `mapstructure:"endpoint" yaml:"endpoint,omitempty"`
|
||||
InsecureSkipVerify bool `mapstructure:"insecure" yaml:"insecure,omitempty"`
|
||||
}
|
||||
|
||||
func (s *S3Cache) Configure(cacheInfo CacheProvider) error {
|
||||
if cacheInfo.S3.BucketName == "" {
|
||||
log.Fatal("Bucket name not configured")
|
||||
}
|
||||
if cacheInfo.S3.Region == "" {
|
||||
log.Fatal("Region not configured")
|
||||
}
|
||||
s.bucketName = cacheInfo.S3.BucketName
|
||||
|
||||
sess := session.Must(session.NewSessionWithOptions(session.Options{
|
||||
@@ -36,6 +37,15 @@ func (s *S3Cache) Configure(cacheInfo CacheProvider) error {
|
||||
Region: aws.String(cacheInfo.S3.Region),
|
||||
},
|
||||
}))
|
||||
if cacheInfo.S3.Endpoint != "" {
|
||||
sess.Config.Endpoint = &cacheInfo.S3.Endpoint
|
||||
sess.Config.S3ForcePathStyle = aws.Bool(true)
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: cacheInfo.S3.InsecureSkipVerify},
|
||||
}
|
||||
customClient := &http.Client{Transport: transport}
|
||||
sess.Config.HTTPClient = customClient
|
||||
}
|
||||
|
||||
s3Client := s3.New(sess)
|
||||
|
||||
@@ -90,9 +100,9 @@ func (s *S3Cache) Load(key string) (string, error) {
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(result.Body)
|
||||
_, err_read := buf.ReadFrom(result.Body)
|
||||
result.Body.Close()
|
||||
return buf.String(), nil
|
||||
return buf.String(), err_read
|
||||
}
|
||||
|
||||
func (s *S3Cache) List() ([]CacheObjectDetails, error) {
|
||||
|
||||
8
pkg/cache/types.go
vendored
8
pkg/cache/types.go
vendored
@@ -3,9 +3,11 @@ package cache
|
||||
import "time"
|
||||
|
||||
type CacheProvider struct {
|
||||
GCS GCSCacheConfiguration `mapstructucre:"gcs" yaml:"gcs,omitempty"`
|
||||
Azure AzureCacheConfiguration `mapstructucre:"azure" yaml:"azure,omitempty"`
|
||||
S3 S3CacheConfiguration `mapstructucre:"s3" yaml:"s3,omitempty"`
|
||||
CurrentCacheType string `mapstructure:"currentCacheType" yaml:"currentCacheType"`
|
||||
GCS GCSCacheConfiguration `mapstructure:"gcs" yaml:"gcs,omitempty"`
|
||||
Azure AzureCacheConfiguration `mapstructure:"azure" yaml:"azure,omitempty"`
|
||||
S3 S3CacheConfiguration `mapstructure:"s3" yaml:"s3,omitempty"`
|
||||
Interplex InterplexCacheConfiguration `mapstructure:"interplex" yaml:"interplex,omitempty"`
|
||||
}
|
||||
|
||||
type CacheObjectDetails struct {
|
||||
|
||||
@@ -15,14 +15,17 @@ package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
trivy "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
|
||||
openapi_v2 "github.com/google/gnostic/openapiv2"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
keda "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
|
||||
kyverno "github.com/kyverno/policy-reporter-kyverno-plugin/pkg/crd/api/policyreport/v1alpha2"
|
||||
regv1 "k8s.io/api/admissionregistration/v1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autov1 "k8s.io/api/autoscaling/v1"
|
||||
autov2 "k8s.io/api/autoscaling/v2"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
networkv1 "k8s.io/api/networking/v1"
|
||||
policyv1 "k8s.io/api/policy/v1"
|
||||
@@ -37,6 +40,7 @@ type Analyzer struct {
|
||||
Client *kubernetes.Client
|
||||
Context context.Context
|
||||
Namespace string
|
||||
LabelSelector string
|
||||
AIClient ai.IAI
|
||||
PreAnalysis map[string]PreAnalysis
|
||||
Results []Result
|
||||
@@ -51,7 +55,7 @@ type PreAnalysis struct {
|
||||
PersistentVolumeClaim v1.PersistentVolumeClaim
|
||||
Endpoint v1.Endpoints
|
||||
Ingress networkv1.Ingress
|
||||
HorizontalPodAutoscalers autov1.HorizontalPodAutoscaler
|
||||
HorizontalPodAutoscalers autov2.HorizontalPodAutoscaler
|
||||
PodDisruptionBudget policyv1.PodDisruptionBudget
|
||||
StatefulSet appsv1.StatefulSet
|
||||
NetworkPolicy networkv1.NetworkPolicy
|
||||
@@ -62,8 +66,11 @@ type PreAnalysis struct {
|
||||
Gateway gtwapi.Gateway
|
||||
HTTPRoute gtwapi.HTTPRoute
|
||||
// Integrations
|
||||
TrivyVulnerabilityReport trivy.VulnerabilityReport
|
||||
TrivyConfigAuditReport trivy.ConfigAuditReport
|
||||
ScaledObject keda.ScaledObject
|
||||
TrivyVulnerabilityReport trivy.VulnerabilityReport
|
||||
TrivyConfigAuditReport trivy.ConfigAuditReport
|
||||
KyvernoPolicyReport kyverno.PolicyReport
|
||||
KyvernoClusterPolicyReport kyverno.ClusterPolicyReport
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -74,6 +81,11 @@ type Result struct {
|
||||
ParentObject string `json:"parentObject"`
|
||||
}
|
||||
|
||||
type AnalysisStats struct {
|
||||
Analyzer string `json:"analyzer"`
|
||||
DurationTime time.Duration `json:"durationTime"`
|
||||
}
|
||||
|
||||
type Failure struct {
|
||||
Text string
|
||||
KubernetesDoc string
|
||||
|
||||
58
pkg/custom/client.go
Normal file
58
pkg/custom/client.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package custom
|
||||
|
||||
import (
|
||||
rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc"
|
||||
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
c *grpc.ClientConn
|
||||
analyzerClient rpc.CustomAnalyzerServiceClient
|
||||
}
|
||||
|
||||
func NewClient(c Connection) (*Client, error) {
|
||||
|
||||
//nolint:staticcheck // Ignoring SA1019 for compatibility reasons
|
||||
conn, err := grpc.Dial(fmt.Sprintf("%s:%s", c.Url, c.Port), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := rpc.NewCustomAnalyzerServiceClient(conn)
|
||||
return &Client{
|
||||
c: conn,
|
||||
analyzerClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cli *Client) Run() (common.Result, error) {
|
||||
var result common.Result
|
||||
req := &schemav1.RunRequest{}
|
||||
res, err := cli.analyzerClient.Run(context.Background(), req)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if res.Result != nil {
|
||||
|
||||
// We should refactor this, because Error and Failure do not map 1:1 from K8sGPT/schema
|
||||
var errorsFound []common.Failure
|
||||
for _, e := range res.Result.Error {
|
||||
errorsFound = append(errorsFound, common.Failure{
|
||||
Text: e.Text,
|
||||
// TODO: Support sensitive data
|
||||
})
|
||||
}
|
||||
|
||||
result.Name = res.Result.Name
|
||||
result.Kind = res.Result.Kind
|
||||
result.Details = res.Result.Details
|
||||
result.ParentObject = res.Result.ParentObject
|
||||
result.Error = errorsFound
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user