mirror of
https://github.com/k8sgpt-ai/k8sgpt.git
synced 2026-03-19 03:23:47 +00:00
Compare commits
403 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
eb32a0f2bc | ||
|
|
ec08cac214 | ||
|
|
dca5b4710d | ||
|
|
b1c791a396 | ||
|
|
b780105807 | ||
|
|
16469c01c9 | ||
|
|
cf1e243708 | ||
|
|
b5853de8a6 | ||
|
|
58d182e94f | ||
|
|
5b27c3e352 | ||
|
|
3e3f6a903a | ||
|
|
12146bf356 | ||
|
|
beaa53251c | ||
|
|
e2bb567d2f | ||
|
|
24132c2d87 | ||
|
|
11e568c6c2 | ||
|
|
9971699fcf | ||
|
|
531fa79ed6 | ||
|
|
2aa31bc66d | ||
|
|
84df3640bc | ||
|
|
73682717ed | ||
|
|
45312788c3 | ||
|
|
c78c4f0cb6 | ||
|
|
d365886753 | ||
|
|
4408110b1a | ||
|
|
c4925b2170 | ||
|
|
66ebb88efe | ||
|
|
1a554ae840 | ||
|
|
19e502a841 | ||
|
|
fdb2934e8f | ||
|
|
45ebad7b4d | ||
|
|
6a665f05d7 | ||
|
|
d359caaab6 | ||
|
|
9dadd186c8 | ||
|
|
3bff9cbe7b | ||
|
|
05f444dec1 | ||
|
|
bb21ce80c7 | ||
|
|
48486e9627 | ||
|
|
120027e3cb | ||
|
|
9c518badf5 | ||
|
|
8ab26d96ce | ||
|
|
4978fe2523 | ||
|
|
ccef7f6170 | ||
|
|
188a8a2cd5 | ||
|
|
37721b5dd7 | ||
|
|
16b229d547 | ||
|
|
901c5ec188 | ||
|
|
40133adaed | ||
|
|
0136b8f543 | ||
|
|
f4b361aed6 | ||
|
|
f1a7801e9e | ||
|
|
4af0ad0303 | ||
|
|
a77bd41048 | ||
|
|
63a226065c | ||
|
|
0e7219a36a | ||
|
|
7e73f8afbc | ||
|
|
23ac52d5ff | ||
|
|
c977528ec7 | ||
|
|
b5facd64a3 | ||
|
|
1d7360c0ae | ||
|
|
184d148108 | ||
|
|
3ebc86772d | ||
|
|
c5c198e3d6 | ||
|
|
539ca3b78f | ||
|
|
2a34ff24d1 | ||
|
|
6d3038b0e8 | ||
|
|
3f36a44415 | ||
|
|
923a8c13c0 | ||
|
|
95c8cc0afb | ||
|
|
c5a8c46298 | ||
|
|
2494946dc8 | ||
|
|
f11d3149b2 | ||
|
|
20e6bd816f | ||
|
|
85ce557681 | ||
|
|
87c8bcea4b | ||
|
|
4d4e33bea9 | ||
|
|
9597002723 | ||
|
|
bcd058c3b0 | ||
|
|
69fe2db8ac | ||
|
|
ddeff9fae4 | ||
|
|
aa9e6a3549 | ||
|
|
e1a42ff3bc | ||
|
|
901ffb8df4 | ||
|
|
402e97d05e | ||
|
|
1da4b7c8f0 | ||
|
|
820e4755a5 | ||
|
|
cad605af46 | ||
|
|
9bd11feb01 | ||
|
|
7461a748f8 | ||
|
|
b4656f533b | ||
|
|
4bfc7f996c | ||
|
|
844ff1fc78 | ||
|
|
d6b7b818ae | ||
|
|
f55946d60e | ||
|
|
54caff837d | ||
|
|
3277b2ad4b | ||
|
|
f60736035b | ||
|
|
1b80b0ce95 | ||
|
|
2472da1673 | ||
|
|
ab064b940c | ||
|
|
6481590b29 | ||
|
|
0325724658 | ||
|
|
f76b572654 | ||
|
|
abfb58432f | ||
|
|
d58e002d7d | ||
|
|
ac2937c821 | ||
|
|
6c5a0628e4 | ||
|
|
598ef22e57 | ||
|
|
95787f2854 | ||
|
|
230eace187 | ||
|
|
b491c9200e | ||
|
|
00d7a27ec1 | ||
|
|
241f1bd6df | ||
|
|
25890e6e38 | ||
|
|
3aabb4842d | ||
|
|
5de3b64098 | ||
|
|
5f73240a06 | ||
|
|
44d17c51ff | ||
|
|
2c81dadb4d | ||
|
|
067c3483e6 | ||
|
|
e65d9a6505 | ||
|
|
c24825b810 | ||
|
|
b6498ef269 | ||
|
|
133850f984 | ||
|
|
09984c245d | ||
|
|
1a7f45cc55 | ||
|
|
fc90dc865b | ||
|
|
632fc9a99f | ||
|
|
96d97cfa30 | ||
|
|
8f0a2fd41d | ||
|
|
5e17e66665 | ||
|
|
6f9f7b2b60 | ||
|
|
95ebc52a6b | ||
|
|
44d3613c1f | ||
|
|
0e5be89e5c | ||
|
|
13f63eda2a | ||
|
|
cc665ea4f3 | ||
|
|
aee83b74b2 | ||
|
|
e3b21ec5ec | ||
|
|
1a0ae1a086 | ||
|
|
610720a95c | ||
|
|
7910c9aa2c | ||
|
|
f8a53a5c03 | ||
|
|
81fcf8b5d4 | ||
|
|
2251321696 | ||
|
|
9672cea228 | ||
|
|
7f109cdcfa | ||
|
|
9802e82ff5 | ||
|
|
9acaec00c4 | ||
|
|
fe29361e33 | ||
|
|
f1479babba | ||
|
|
c2770f38a6 | ||
|
|
59897f330a | ||
|
|
aafac9345f | ||
|
|
227e1cd69f | ||
|
|
5bb91ff2c9 | ||
|
|
43b0d707e7 | ||
|
|
c3640744c5 | ||
|
|
3cc7aa56d8 | ||
|
|
7e8668a56b | ||
|
|
b52424a9b1 | ||
|
|
fba1a8ed8c | ||
|
|
8ae91ec744 | ||
|
|
cf9069ef57 | ||
|
|
5ee8499e3d | ||
|
|
153d38deb0 | ||
|
|
9d2bee9b7d | ||
|
|
70bec050d8 | ||
|
|
781ecb7aad | ||
|
|
c42221512b | ||
|
|
2e0db553f9 | ||
|
|
cc83fe19ba | ||
|
|
dc463334bc | ||
|
|
9eb96c495c | ||
|
|
ee83aa1e9e | ||
|
|
06e8532f88 | ||
|
|
1a3f299210 | ||
|
|
731e1520ec | ||
|
|
f844f7308e | ||
|
|
750a10d44c | ||
|
|
9dcab94546 | ||
|
|
ecd7790efe | ||
|
|
d1096dc31a | ||
|
|
21df094bda | ||
|
|
1276b3e897 | ||
|
|
ad112eae2e | ||
|
|
da750df16b | ||
|
|
767e4cbc41 | ||
|
|
3067fa98f4 | ||
|
|
2309b0dfe2 | ||
|
|
1f5462c80b | ||
|
|
5a983c4a0a | ||
|
|
5662d5932f | ||
|
|
367fe8f74c | ||
|
|
566f7525ee | ||
|
|
fafb69544f | ||
|
|
b7d4602cb8 | ||
|
|
d56861d4ba | ||
|
|
3146754779 | ||
|
|
57695b44b6 | ||
|
|
be52308c99 | ||
|
|
cd7807a484 | ||
|
|
0521060410 | ||
|
|
b9cf522685 | ||
|
|
06e50d57db | ||
|
|
c88fc889e4 | ||
|
|
c15a561b63 | ||
|
|
1819c3bf15 | ||
|
|
f4d7876838 | ||
|
|
d87127a309 | ||
|
|
9df75cc959 | ||
|
|
f3e0b9b56d | ||
|
|
0f03ddcf0f | ||
|
|
14e5691190 | ||
|
|
64b93c9116 | ||
|
|
4038318adc | ||
|
|
b1c6ec3c09 | ||
|
|
f6db6ce861 |
20
.github/workflows/build_container.yaml
vendored
20
.github/workflows/build_container.yaml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
- "**.md"
|
||||
|
||||
env:
|
||||
GO_VERSION: "~1.20"
|
||||
GO_VERSION: "~1.21"
|
||||
IMAGE_NAME: "k8sgpt"
|
||||
defaults:
|
||||
run:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract_branch
|
||||
@@ -70,14 +70,14 @@ jobs:
|
||||
RELEASE_REGISTRY: "localhost:5000/k8sgpt"
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@6a58db7e0d21ca03e6c44877909e80e45217eed2 # v2
|
||||
uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@44ea916f6c540f9302d50c2b1e5a8dc071f15cdf # v4
|
||||
uses: docker/build-push-action@af5a7ed5ba88268d5278f7203fb52cd833f66d6e # v5
|
||||
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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4
|
||||
with:
|
||||
name: ${{ env.IMAGE_NAME }}-image.tar
|
||||
path: /tmp/${{ env.IMAGE_NAME }}-image.tar
|
||||
@@ -115,10 +115,10 @@ jobs:
|
||||
contents: read # Needed for checking out the repository
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2
|
||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # 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@6a58db7e0d21ca03e6c44877909e80e45217eed2 # v2
|
||||
uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@44ea916f6c540f9302d50c2b1e5a8dc071f15cdf # v4
|
||||
uses: docker/build-push-action@af5a7ed5ba88268d5278f7203fb52cd833f66d6e # v5
|
||||
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@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: golangci-lint
|
||||
uses: reviewdog/action-golangci-lint@79d32f10b2ea0d4cebb755d849b048c4b40c3d50 # v2
|
||||
uses: reviewdog/action-golangci-lint@00311c26a97213f93f2fd3a3524d66762e956ae0 # v2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-pr-check
|
||||
golangci_lint_flags: "--timeout=240s"
|
||||
level: warning
|
||||
|
||||
28
.github/workflows/release.yaml
vendored
28
.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@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
|
||||
- uses: google-github-actions/release-please-action@51ee8ae2605bd5ce1cfdcc5938684908f1cd9f69 # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- uses: google-github-actions/release-please-action@cc61a07e2da466bebbc19b3a7dd01d6aecb20d1e # v4.0.2
|
||||
id: release
|
||||
with:
|
||||
command: manifest
|
||||
@@ -41,17 +41,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4
|
||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
|
||||
with:
|
||||
go-version: '1.20'
|
||||
go-version: '1.21'
|
||||
- name: Download Syft
|
||||
uses: anchore/sbom-action/download-syft@4d571ad1038a9cc29d676154ef265ab8f9027042 # v0.14.2
|
||||
uses: anchore/sbom-action/download-syft@ab5d7b5f48981941c4c5d6bf33aeb98fe3bae38c # v0.15.10
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@336e29918d653399e599bfca99fadc1d7ffbc9f7 # v4
|
||||
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5
|
||||
with:
|
||||
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||
distribution: goreleaser
|
||||
@@ -74,23 +74,23 @@ jobs:
|
||||
IMAGE_NAME: k8sgpt
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@6a58db7e0d21ca03e6c44877909e80e45217eed2 # v2
|
||||
uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2
|
||||
uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@44ea916f6c540f9302d50c2b1e5a8dc071f15cdf # v4
|
||||
uses: docker/build-push-action@af5a7ed5ba88268d5278f7203fb52cd833f66d6e # v5
|
||||
with:
|
||||
context: .
|
||||
file: ./container/Dockerfile
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_TAG }}
|
||||
|
||||
- name: Generate SBOM
|
||||
uses: anchore/sbom-action@4d571ad1038a9cc29d676154ef265ab8f9027042 # v0.14.2
|
||||
uses: anchore/sbom-action@ab5d7b5f48981941c4c5d6bf33aeb98fe3bae38c # v0.15.10
|
||||
with:
|
||||
image: ${{ env.IMAGE_TAG }}
|
||||
artifact-name: sbom-${{ env.IMAGE_NAME }}
|
||||
@@ -114,4 +114,4 @@ jobs:
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
with:
|
||||
tag_name: ${{ needs.release-please.outputs.tag_name }}
|
||||
files: ./sbom-${{ env.IMAGE_NAME }}.spdx.json
|
||||
files: ./sbom-${{ env.IMAGE_NAME }}.spdx.json
|
||||
|
||||
2
.github/workflows/semantic_pr.yaml
vendored
2
.github/workflows/semantic_pr.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
pull-requests: read # Needed for reading prs
|
||||
steps:
|
||||
- name: Validate Pull Request
|
||||
uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 # v5.2.0
|
||||
uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f # v5.4.0
|
||||
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.21"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4
|
||||
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # 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@v3
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
.idea
|
||||
__debug*
|
||||
.DS_Store
|
||||
k8sgpt*
|
||||
!charts/k8sgpt
|
||||
|
||||
@@ -23,7 +23,7 @@ nfpms:
|
||||
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 +32,7 @@ nfpms:
|
||||
section: utils
|
||||
contents:
|
||||
- src: ./LICENSE
|
||||
dst: /usr/share/doc/nfpm/copyright
|
||||
dst: /usr/share/doc/k8sgpt/copyright
|
||||
file_info:
|
||||
mode: 0644
|
||||
|
||||
@@ -57,7 +57,7 @@ archives:
|
||||
brews:
|
||||
- name: k8sgpt
|
||||
homepage: https://k8sgpt.ai
|
||||
tap:
|
||||
repository:
|
||||
owner: k8sgpt-ai
|
||||
name: homebrew-k8sgpt
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{".":"0.3.7"}
|
||||
{".":"0.3.30"}
|
||||
673
CHANGELOG.md
673
CHANGELOG.md
@@ -1,5 +1,678 @@
|
||||
# Changelog
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Gateway analysers ([#764](https://github.com/k8sgpt-ai/k8sgpt/issues/764)) ([ec08cac](https://github.com/k8sgpt-ai/k8sgpt/commit/ec08cac21496b34b123b75b06d9283eb6539e890))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.48.3 ([#768](https://github.com/k8sgpt-ai/k8sgpt/issues/768)) ([b1c791a](https://github.com/k8sgpt-ai/k8sgpt/commit/b1c791a396b7287ef916e8f8d382a0e14ba39949))
|
||||
* **deps:** update module github.com/mittwald/go-helm-client to v0.12.4 ([#767](https://github.com/k8sgpt-ai/k8sgpt/issues/767)) ([dca5b47](https://github.com/k8sgpt-ai/k8sgpt/commit/dca5b4710d1bb35dfc3346219d3bddb7c726300e))
|
||||
|
||||
## [0.3.22](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.21...v0.3.22) (2023-11-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* rework cache package - add gcs cache - add cache purge command ([#750](https://github.com/k8sgpt-ai/k8sgpt/issues/750)) ([12146bf](https://github.com/k8sgpt-ai/k8sgpt/commit/12146bf356a3b26176c47e3a013a713fd14f346d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cover more error reason messages ([#759](https://github.com/k8sgpt-ai/k8sgpt/issues/759)) ([5b27c3e](https://github.com/k8sgpt-ai/k8sgpt/commit/5b27c3e352701819f1d0449df9acf706040f1f13))
|
||||
* **deps:** update kubernetes packages to v0.28.4 ([#756](https://github.com/k8sgpt-ai/k8sgpt/issues/756)) ([24132c2](https://github.com/k8sgpt-ai/k8sgpt/commit/24132c2d87024157009589cf2bd410bac2a26241))
|
||||
* **deps:** update module cloud.google.com/go/storage to v1.35.1 ([#762](https://github.com/k8sgpt-ai/k8sgpt/issues/762)) ([58d182e](https://github.com/k8sgpt-ai/k8sgpt/commit/58d182e94f75f9b035a9e45159fa87ce8a57de38))
|
||||
* **deps:** update module github.com/aquasecurity/trivy-operator to v0.16.4 ([#676](https://github.com/k8sgpt-ai/k8sgpt/issues/676)) ([4531278](https://github.com/k8sgpt-ai/k8sgpt/commit/45312788c3c15e141027c3fc8e428cfaa71d3ace))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.47.10 ([#751](https://github.com/k8sgpt-ai/k8sgpt/issues/751)) ([2aa31bc](https://github.com/k8sgpt-ai/k8sgpt/commit/2aa31bc66d239906b1047f53bcaa58b0c30a2856))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.47.11 ([#752](https://github.com/k8sgpt-ai/k8sgpt/issues/752)) ([531fa79](https://github.com/k8sgpt-ai/k8sgpt/commit/531fa79ed640846b177c516559dc82f088fa940f))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.48.0 ([#754](https://github.com/k8sgpt-ai/k8sgpt/issues/754)) ([e2bb567](https://github.com/k8sgpt-ai/k8sgpt/commit/e2bb567d2f8d59a904583309c2774d4174eb367f))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.48.1 ([#766](https://github.com/k8sgpt-ai/k8sgpt/issues/766)) ([16469c0](https://github.com/k8sgpt-ai/k8sgpt/commit/16469c01c962fd5bfa4ad11dd88a41f3e00e4a0d))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.17.6 ([#749](https://github.com/k8sgpt-ai/k8sgpt/issues/749)) ([84df364](https://github.com/k8sgpt-ai/k8sgpt/commit/84df3640bc114bb2c768f158d3575732103ff799))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.17.7 ([#753](https://github.com/k8sgpt-ai/k8sgpt/issues/753)) ([9971699](https://github.com/k8sgpt-ai/k8sgpt/commit/9971699fcf42b3309449d81875d45180f723de8d))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.17.8 ([#761](https://github.com/k8sgpt-ai/k8sgpt/issues/761)) ([beaa532](https://github.com/k8sgpt-ai/k8sgpt/commit/beaa53251c8201028db83d60f208e2b0658c93d8))
|
||||
* **deps:** update module google.golang.org/api to v0.151.0 ([#763](https://github.com/k8sgpt-ai/k8sgpt/issues/763)) ([3e3f6a9](https://github.com/k8sgpt-ai/k8sgpt/commit/3e3f6a903a81d9622660f5adf9cae7d22a5c99f4))
|
||||
* show trivy as active when activated with --no-install flag ([#675](https://github.com/k8sgpt-ai/k8sgpt/issues/675)) ([7368271](https://github.com/k8sgpt-ai/k8sgpt/commit/73682717eda4fa2e0cbc6311d5c97e01e0f2673c))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update anchore/sbom-action action to v0.15.0 ([#765](https://github.com/k8sgpt-ai/k8sgpt/issues/765)) ([cf1e243](https://github.com/k8sgpt-ai/k8sgpt/commit/cf1e243708ab406f070da3f96be1fc60b7ce2ea4))
|
||||
* **deps:** update docker/build-push-action digest to 4a13e50 ([#760](https://github.com/k8sgpt-ai/k8sgpt/issues/760)) ([b5853de](https://github.com/k8sgpt-ai/k8sgpt/commit/b5853de8a6fcd17b1c1a4c53dbe3ffc82b83f72f))
|
||||
|
||||
## [0.3.21](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.20...v0.3.21) (2023-11-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* auth remove: add -b flag ([#711](https://github.com/k8sgpt-ai/k8sgpt/issues/711)) ([9dadd18](https://github.com/k8sgpt-ai/k8sgpt/commit/9dadd186c8d03a4284faff3f0842d6e2d00ebbb8))
|
||||
* log analyzer ([#744](https://github.com/k8sgpt-ai/k8sgpt/issues/744)) ([d365886](https://github.com/k8sgpt-ai/k8sgpt/commit/d365886753f785bd58118c03510696318ea47941))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.47.6 ([#728](https://github.com/k8sgpt-ai/k8sgpt/issues/728)) ([bb21ce8](https://github.com/k8sgpt-ai/k8sgpt/commit/bb21ce80c782e011dfa1f808ccdd82ae748bfed8))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.47.8 ([#741](https://github.com/k8sgpt-ai/k8sgpt/issues/741)) ([d359caa](https://github.com/k8sgpt-ai/k8sgpt/commit/d359caaab6bdb42a54d305be2f4cd8452f512bb8))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.47.9 ([#743](https://github.com/k8sgpt-ai/k8sgpt/issues/743)) ([45ebad7](https://github.com/k8sgpt-ai/k8sgpt/commit/45ebad7b4d80d93920d5fbad9f42c8fcd45218bd))
|
||||
* **deps:** update module github.com/fatih/color to v1.16.0 ([#734](https://github.com/k8sgpt-ai/k8sgpt/issues/734)) ([8ab26d9](https://github.com/k8sgpt-ai/k8sgpt/commit/8ab26d96cec73369ecf014d50fccc26afe15fa44))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.17.3 ([#737](https://github.com/k8sgpt-ai/k8sgpt/issues/737)) ([48486e9](https://github.com/k8sgpt-ai/k8sgpt/commit/48486e96274a5e52a03cef00bd531148e27b38c5))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.17.5 ([#742](https://github.com/k8sgpt-ai/k8sgpt/issues/742)) ([3bff9cb](https://github.com/k8sgpt-ai/k8sgpt/commit/3bff9cbe7bb3afb7212735eb91902fd83d3cbb8c))
|
||||
* **deps:** update module github.com/schollz/progressbar/v3 to v3.14.1 ([#738](https://github.com/k8sgpt-ai/k8sgpt/issues/738)) ([05f444d](https://github.com/k8sgpt-ai/k8sgpt/commit/05f444dec1f234c191e25f71f3eab4838eb2477a))
|
||||
* **deps:** update module github.com/spf13/cobra to v1.8.0 ([#732](https://github.com/k8sgpt-ai/k8sgpt/issues/732)) ([19e502a](https://github.com/k8sgpt-ai/k8sgpt/commit/19e502a841e0463b682b0c6b8291f10aee616d7e))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.13.2 ([#740](https://github.com/k8sgpt-ai/k8sgpt/issues/740)) ([6a665f0](https://github.com/k8sgpt-ai/k8sgpt/commit/6a665f05d782ba9c3051df7a15ff304c89cb34f4))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** pin google-github-actions/release-please-action action to db8f2c6 ([#747](https://github.com/k8sgpt-ai/k8sgpt/issues/747)) ([4408110](https://github.com/k8sgpt-ai/k8sgpt/commit/4408110b1a4835bb237b3d5674d6fa8a13f0181b))
|
||||
* **deps:** update google-github-actions/release-please-action digest to 4c5670f ([#721](https://github.com/k8sgpt-ai/k8sgpt/issues/721)) ([9c518ba](https://github.com/k8sgpt-ai/k8sgpt/commit/9c518badf53e4ccd9c2f9251cead4692602c0762))
|
||||
* **deps:** update google-github-actions/release-please-action digest to db8f2c6 ([#736](https://github.com/k8sgpt-ai/k8sgpt/issues/736)) ([fdb2934](https://github.com/k8sgpt-ai/k8sgpt/commit/fdb2934e8fd02bcb4e47b34c1eca5b099f462faa))
|
||||
* enable automerge for renovate ([#745](https://github.com/k8sgpt-ai/k8sgpt/issues/745)) ([66ebb88](https://github.com/k8sgpt-ai/k8sgpt/commit/66ebb88efe1ad5ecae75a5299f58a1e68179b515))
|
||||
* pin release-please version ([#746](https://github.com/k8sgpt-ai/k8sgpt/issues/746)) ([c4925b2](https://github.com/k8sgpt-ai/k8sgpt/commit/c4925b2170546d0d86b77d2a13c13d4907e2e3d6))
|
||||
|
||||
|
||||
### Dependency Updates
|
||||
|
||||
* bump docker fixes CVE GHSA-jq35-85cj-fj4p ([#733](https://github.com/k8sgpt-ai/k8sgpt/issues/733)) ([120027e](https://github.com/k8sgpt-ai/k8sgpt/commit/120027e3cbec2535f0b6cc8d8db1dc27dd9f3ec6))
|
||||
|
||||
## [0.3.20](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.19...v0.3.20) (2023-11-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* amazonsagemaker AI provider ([#731](https://github.com/k8sgpt-ai/k8sgpt/issues/731)) ([ccef7f6](https://github.com/k8sgpt-ai/k8sgpt/commit/ccef7f617004723b37d1e8ffb011398005e0b392))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.47.1 ([#724](https://github.com/k8sgpt-ai/k8sgpt/issues/724)) ([0136b8f](https://github.com/k8sgpt-ai/k8sgpt/commit/0136b8f543a7052e967e29691afe1aab8e5fae1b))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.2.0 ([#723](https://github.com/k8sgpt-ai/k8sgpt/issues/723)) ([16b229d](https://github.com/k8sgpt-ai/k8sgpt/commit/16b229d5478085655041ff0230d2542c4c0c7ce9))
|
||||
* **deps:** update module google.golang.org/grpc to v1.59.0 ([#713](https://github.com/k8sgpt-ai/k8sgpt/issues/713)) ([901c5ec](https://github.com/k8sgpt-ai/k8sgpt/commit/901c5ec18858f2f7fd385ff20aef77d203748c93))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.13.1 ([#706](https://github.com/k8sgpt-ai/k8sgpt/issues/706)) ([40133ad](https://github.com/k8sgpt-ai/k8sgpt/commit/40133adaedff3862199e00f62877a88fcffa67c5))
|
||||
* ensure ingress HTTP rule exists to prevent panic ([#726](https://github.com/k8sgpt-ai/k8sgpt/issues/726)) ([37721b5](https://github.com/k8sgpt-ai/k8sgpt/commit/37721b5dd77d66edfb7e8377b2b96470b8a21d1b))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update amannn/action-semantic-pull-request action to v5.4.0 ([#729](https://github.com/k8sgpt-ai/k8sgpt/issues/729)) ([188a8a2](https://github.com/k8sgpt-ai/k8sgpt/commit/188a8a2cd5e25b35446e2eab46279a0ba3976af3))
|
||||
|
||||
## [0.3.19](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.18...v0.3.19) (2023-10-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add amazonbedrock ([#718](https://github.com/k8sgpt-ai/k8sgpt/issues/718)) ([f1a7801](https://github.com/k8sgpt-ai/k8sgpt/commit/f1a7801e9e6a7e4a5310622951dfba3ba3acd047))
|
||||
* add Azure remote cache ([#690](https://github.com/k8sgpt-ai/k8sgpt/issues/690)) ([23ac52d](https://github.com/k8sgpt-ai/k8sgpt/commit/23ac52d5ffc0b2ebb7516b070fa740108cb4299a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update kubernetes packages to v0.28.3 ([#715](https://github.com/k8sgpt-ai/k8sgpt/issues/715)) ([7e73f8a](https://github.com/k8sgpt-ai/k8sgpt/commit/7e73f8afbce7ba0e9de432671b88c01fcfe28c3a))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.25 ([#707](https://github.com/k8sgpt-ai/k8sgpt/issues/707)) ([3ebc867](https://github.com/k8sgpt-ai/k8sgpt/commit/3ebc86772dc8f8cb2d2246724f5fd05d1e931512))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.26 ([#709](https://github.com/k8sgpt-ai/k8sgpt/issues/709)) ([c977528](https://github.com/k8sgpt-ai/k8sgpt/commit/c977528ec7839902570785e0803f6c6b83a0a69d))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.46.5 ([#712](https://github.com/k8sgpt-ai/k8sgpt/issues/712)) ([63a2260](https://github.com/k8sgpt-ai/k8sgpt/commit/63a226065c8068f9bdc0aa791a325fa10bba3fcc))
|
||||
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.4.0 ([#722](https://github.com/k8sgpt-ai/k8sgpt/issues/722)) ([0e7219a](https://github.com/k8sgpt-ai/k8sgpt/commit/0e7219a36aaa718b7d86adf0a218a521bfac119b))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.16.0 ([#703](https://github.com/k8sgpt-ai/k8sgpt/issues/703)) ([b5facd6](https://github.com/k8sgpt-ai/k8sgpt/commit/b5facd64a340a96d38faf045bbb889b928ef08a1))
|
||||
* **deps:** update module github.com/spf13/viper to v1.17.0 ([#700](https://github.com/k8sgpt-ai/k8sgpt/issues/700)) ([184d148](https://github.com/k8sgpt-ai/k8sgpt/commit/184d1481081f4297bec21fbd60d7eff1964944ae))
|
||||
* **deps:** update module google.golang.org/grpc to v1.58.3 ([#704](https://github.com/k8sgpt-ai/k8sgpt/issues/704)) ([1d7360c](https://github.com/k8sgpt-ai/k8sgpt/commit/1d7360c0ae4dab376872acc71dc68d59eb4d9752))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/checkout digest to b4ffde6 ([#719](https://github.com/k8sgpt-ai/k8sgpt/issues/719)) ([a77bd41](https://github.com/k8sgpt-ai/k8sgpt/commit/a77bd410489e624d29ccc8fd45a004f6844b3620))
|
||||
* **deps:** update module oras.land/oras-go to v1.2.4 ([#665](https://github.com/k8sgpt-ai/k8sgpt/issues/665)) ([4af0ad0](https://github.com/k8sgpt-ai/k8sgpt/commit/4af0ad0303d9b0ffb43f1e87fb5abe279d9a8724))
|
||||
|
||||
## [0.3.18](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.17...v0.3.18) (2023-10-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adding temperature to server mode ([#705](https://github.com/k8sgpt-ai/k8sgpt/issues/705)) ([539ca3b](https://github.com/k8sgpt-ai/k8sgpt/commit/539ca3b78f96694c11f788255d3b83d2fb335df4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20231002095256-194bc640518b.1 ([#692](https://github.com/k8sgpt-ai/k8sgpt/issues/692)) ([4d4e33b](https://github.com/k8sgpt-ai/k8sgpt/commit/4d4e33bea9cc4f5f9bf5379db5b890d9ba86e0a9))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go to v1.31.0-20231002095256-194bc640518b.1 ([#693](https://github.com/k8sgpt-ai/k8sgpt/issues/693)) ([20e6bd8](https://github.com/k8sgpt-ai/k8sgpt/commit/20e6bd816f636d4e4c8274d417870ec28fdd8a56))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.20 ([#685](https://github.com/k8sgpt-ai/k8sgpt/issues/685)) ([2494946](https://github.com/k8sgpt-ai/k8sgpt/commit/2494946dc867a532460bd6aac74dfb7da5184c1c))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.21 ([#696](https://github.com/k8sgpt-ai/k8sgpt/issues/696)) ([95c8cc0](https://github.com/k8sgpt-ai/k8sgpt/commit/95c8cc0afb0bb7b99784dcc5ba155f94b5a7dbdf))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.22 ([#697](https://github.com/k8sgpt-ai/k8sgpt/issues/697)) ([923a8c1](https://github.com/k8sgpt-ai/k8sgpt/commit/923a8c13c06b152d04e8b00ab002e2036bf12740))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.23 ([#699](https://github.com/k8sgpt-ai/k8sgpt/issues/699)) ([3f36a44](https://github.com/k8sgpt-ai/k8sgpt/commit/3f36a4441532e3d0ac1bd9d00fc738d4902b23a8))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.24 ([#701](https://github.com/k8sgpt-ai/k8sgpt/issues/701)) ([6d3038b](https://github.com/k8sgpt-ai/k8sgpt/commit/6d3038b0e8336235dc6a2fdb69d2381790331596))
|
||||
* **deps:** update module github.com/prometheus/client_golang to v1.17.0 ([#687](https://github.com/k8sgpt-ai/k8sgpt/issues/687)) ([9597002](https://github.com/k8sgpt-ai/k8sgpt/commit/95970027237e0079ed1f66dc9655fa01b181f4d7))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.15.4 ([#689](https://github.com/k8sgpt-ai/k8sgpt/issues/689)) ([f11d314](https://github.com/k8sgpt-ai/k8sgpt/commit/f11d3149b228b643155ed66c189cb0f8a4dd5a0f))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.13.0 ([#688](https://github.com/k8sgpt-ai/k8sgpt/issues/688)) ([87c8bce](https://github.com/k8sgpt-ai/k8sgpt/commit/87c8bcea4becd165aeb0ac98d79df7dab9c37ee3))
|
||||
* security warning around printing provider details in https://github.com/k8sgpt-ai/k8sgpt/security/code-scanning/1 ([#695](https://github.com/k8sgpt-ai/k8sgpt/issues/695)) ([85ce557](https://github.com/k8sgpt-ai/k8sgpt/commit/85ce55768199f90b1d2a5118ec2621ea5c7a7a67))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update amannn/action-semantic-pull-request action to v5.3.0 ([#683](https://github.com/k8sgpt-ai/k8sgpt/issues/683)) ([c5a8c46](https://github.com/k8sgpt-ai/k8sgpt/commit/c5a8c462989c097bf37ac48ea4f1a9010285042c))
|
||||
* fixing default model issue ([#702](https://github.com/k8sgpt-ai/k8sgpt/issues/702)) ([2a34ff2](https://github.com/k8sgpt-ai/k8sgpt/commit/2a34ff24d1f391270ae42531807cb1422880ad27))
|
||||
|
||||
## [0.3.17](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.16...v0.3.17) (2023-09-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added create namespace on deploy ([#673](https://github.com/k8sgpt-ai/k8sgpt/issues/673)) ([820e475](https://github.com/k8sgpt-ai/k8sgpt/commit/820e4755a54ecab3b5d800017bf6948dc9212825))
|
||||
* integration refactor ([#684](https://github.com/k8sgpt-ai/k8sgpt/issues/684)) ([69fe2db](https://github.com/k8sgpt-ai/k8sgpt/commit/69fe2db8acb795add27f04c1c8ee8d05819300ac))
|
||||
* update readme with new analyzers ([#671](https://github.com/k8sgpt-ai/k8sgpt/issues/671)) ([cad605a](https://github.com/k8sgpt-ai/k8sgpt/commit/cad605af462ce8b02ffc279ea847e41b7a64196f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update kubernetes packages to v0.28.2 ([#607](https://github.com/k8sgpt-ai/k8sgpt/issues/607)) ([ddeff9f](https://github.com/k8sgpt-ai/k8sgpt/commit/ddeff9fae4e80d1452893c59b89742633eb6b51b))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.14 ([#672](https://github.com/k8sgpt-ai/k8sgpt/issues/672)) ([1da4b7c](https://github.com/k8sgpt-ai/k8sgpt/commit/1da4b7c8f0eee877d5b76a7dd9abda7631d922f3))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.16 ([#682](https://github.com/k8sgpt-ai/k8sgpt/issues/682)) ([e1a42ff](https://github.com/k8sgpt-ai/k8sgpt/commit/e1a42ff3bcb3ddea71df2a5b5288eade024684dc))
|
||||
* **deps:** update module github.com/google/gnostic to v0.7.0 ([#679](https://github.com/k8sgpt-ai/k8sgpt/issues/679)) ([901ffb8](https://github.com/k8sgpt-ai/k8sgpt/commit/901ffb8df451ce41e6dc96da61deab987e51b6df))
|
||||
* **deps:** update module google.golang.org/grpc to v1.58.2 ([#680](https://github.com/k8sgpt-ai/k8sgpt/issues/680)) ([402e97d](https://github.com/k8sgpt-ai/k8sgpt/commit/402e97d05ea33879d997d98019b72da0f1074fc7))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/checkout digest to 8ade135 ([#681](https://github.com/k8sgpt-ai/k8sgpt/issues/681)) ([aa9e6a3](https://github.com/k8sgpt-ai/k8sgpt/commit/aa9e6a3549877260423462c35ebbdfd95381be2c))
|
||||
|
||||
## [0.3.16](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.15...v0.3.16) (2023-09-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* lists activate integrations ([#669](https://github.com/k8sgpt-ai/k8sgpt/issues/669)) ([844ff1f](https://github.com/k8sgpt-ai/k8sgpt/commit/844ff1fc78e7c35837c08b72bd2c19e92698d53d))
|
||||
* openAI explicit value for maxToken and temperature ([#659](https://github.com/k8sgpt-ai/k8sgpt/issues/659)) ([f55946d](https://github.com/k8sgpt-ai/k8sgpt/commit/f55946d60ebc7725aba6702570ca1cb5ba978d78))
|
||||
* serve/integration capability ([#645](https://github.com/k8sgpt-ai/k8sgpt/issues/645)) ([ab064b9](https://github.com/k8sgpt-ai/k8sgpt/commit/ab064b940cdb39a1588816221b20191e68263c61))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20230830164712-dc062a152c20.1 ([#617](https://github.com/k8sgpt-ai/k8sgpt/issues/617)) ([d6b7b81](https://github.com/k8sgpt-ai/k8sgpt/commit/d6b7b818aef1b7775d1e76231077b74481546c56))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.10 ([#657](https://github.com/k8sgpt-ai/k8sgpt/issues/657)) ([0325724](https://github.com/k8sgpt-ai/k8sgpt/commit/03257246589ebbb22961e13394e49b52cb056e38))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.11 ([#662](https://github.com/k8sgpt-ai/k8sgpt/issues/662)) ([1b80b0c](https://github.com/k8sgpt-ai/k8sgpt/commit/1b80b0ce95f39c1cf27ad8bbb05a7fed10322114))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.12 ([#666](https://github.com/k8sgpt-ai/k8sgpt/issues/666)) ([b4656f5](https://github.com/k8sgpt-ai/k8sgpt/commit/b4656f533bdf39d12b223158bf41087076fa6c9a))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.15.3 ([#636](https://github.com/k8sgpt-ai/k8sgpt/issues/636)) ([54caff8](https://github.com/k8sgpt-ai/k8sgpt/commit/54caff837dc25ae594c6cd0e1bd0b31b1612cf73))
|
||||
* **deps:** update module go.uber.org/zap to v1.26.0 ([#658](https://github.com/k8sgpt-ai/k8sgpt/issues/658)) ([f76b572](https://github.com/k8sgpt-ai/k8sgpt/commit/f76b57265432a704c3fc5aa67b0d569179b4ef03))
|
||||
* **deps:** update module google.golang.org/grpc to v1.58.0 ([#635](https://github.com/k8sgpt-ai/k8sgpt/issues/635)) ([d58e002](https://github.com/k8sgpt-ai/k8sgpt/commit/d58e002d7dc55cc759402fcadb03af921cd30dc3))
|
||||
* **deps:** update module google.golang.org/grpc to v1.58.1 ([#656](https://github.com/k8sgpt-ai/k8sgpt/issues/656)) ([abfb584](https://github.com/k8sgpt-ai/k8sgpt/commit/abfb58432fbd1168db13880e5b9dbcbdde70f147))
|
||||
* emergency fix for bad package revision in go mod ([#663](https://github.com/k8sgpt-ai/k8sgpt/issues/663)) ([2472da1](https://github.com/k8sgpt-ai/k8sgpt/commit/2472da167300a831dc5b45f7fc0169a0b5b1ccb7))
|
||||
* pdb panic error guard ([#664](https://github.com/k8sgpt-ai/k8sgpt/issues/664)) ([3277b2a](https://github.com/k8sgpt-ai/k8sgpt/commit/3277b2ad4b27ade9bd7da07f5fc8d8f074355177))
|
||||
* respect namespace scope in trivy analyzer ([#661](https://github.com/k8sgpt-ai/k8sgpt/issues/661)) ([6481590](https://github.com/k8sgpt-ai/k8sgpt/commit/6481590b29b80391ea1c9298cae5d8f0a4ae7354))
|
||||
* use default values when adding auth ([#568](https://github.com/k8sgpt-ai/k8sgpt/issues/568)) ([7461a74](https://github.com/k8sgpt-ai/k8sgpt/commit/7461a748f8e994e58ac4f56fd9919b1744bd7366)), closes [#567](https://github.com/k8sgpt-ai/k8sgpt/issues/567)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/upload-artifact digest to a8a3f3a ([#633](https://github.com/k8sgpt-ai/k8sgpt/issues/633)) ([4bfc7f9](https://github.com/k8sgpt-ai/k8sgpt/commit/4bfc7f996c851adadc5ab0754da6852979084e9d))
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to 24d4af2 ([#642](https://github.com/k8sgpt-ai/k8sgpt/issues/642)) ([f607360](https://github.com/k8sgpt-ai/k8sgpt/commit/f60736035b2601650f4b3ee352f16d1e57d6ec64))
|
||||
|
||||
## [0.3.15](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.14...v0.3.15) (2023-09-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* show each ConfigAuditReport check ([#646](https://github.com/k8sgpt-ai/k8sgpt/issues/646)) ([230eace](https://github.com/k8sgpt-ai/k8sgpt/commit/230eace18737a81e4c023826ffef1a9b1e17d4fd))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* defer to service analyser when selectors are missing ([#652](https://github.com/k8sgpt-ai/k8sgpt/issues/652)) ([6c5a062](https://github.com/k8sgpt-ai/k8sgpt/commit/6c5a0628e4a8c493beae85049448e6e6588d63be))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.333 ([#611](https://github.com/k8sgpt-ai/k8sgpt/issues/611)) ([96d97cf](https://github.com/k8sgpt-ai/k8sgpt/commit/96d97cfa30c4d3c75facda3d3016c080dfa86eaa))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.0 ([#618](https://github.com/k8sgpt-ai/k8sgpt/issues/618)) ([632fc9a](https://github.com/k8sgpt-ai/k8sgpt/commit/632fc9a99fd0482dcff0768211c49bffb2e4032a))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.1 ([#624](https://github.com/k8sgpt-ai/k8sgpt/issues/624)) ([09984c2](https://github.com/k8sgpt-ai/k8sgpt/commit/09984c245de40fc7794f85a9535af4f8e5f5e776))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.2 ([#625](https://github.com/k8sgpt-ai/k8sgpt/issues/625)) ([b6498ef](https://github.com/k8sgpt-ai/k8sgpt/commit/b6498ef269919c61004dd860ebf08ed7f28810f7))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.3 ([#632](https://github.com/k8sgpt-ai/k8sgpt/issues/632)) ([5f73240](https://github.com/k8sgpt-ai/k8sgpt/commit/5f73240a0615e58a37e9eb00784628621bc1dfa1))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.6 ([#634](https://github.com/k8sgpt-ai/k8sgpt/issues/634)) ([3aabb48](https://github.com/k8sgpt-ai/k8sgpt/commit/3aabb4842d96ec14e61842847dc2feb3e3f31a0a))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.45.9 ([#640](https://github.com/k8sgpt-ai/k8sgpt/issues/640)) ([95787f2](https://github.com/k8sgpt-ai/k8sgpt/commit/95787f2854c4e4a971b2d687d97a5ceca30b9d5e))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.15.1 ([#622](https://github.com/k8sgpt-ai/k8sgpt/issues/622)) ([fc90dc8](https://github.com/k8sgpt-ai/k8sgpt/commit/fc90dc865b48fae99253b8bb6a8b1ae7047170b4))
|
||||
* **deps:** update module golang.org/x/term to v0.12.0 ([#626](https://github.com/k8sgpt-ai/k8sgpt/issues/626)) ([44d17c5](https://github.com/k8sgpt-ai/k8sgpt/commit/44d17c51ff8ece92cd0c85f40d15caa97d990544))
|
||||
* typos ([#629](https://github.com/k8sgpt-ai/k8sgpt/issues/629)) ([067c348](https://github.com/k8sgpt-ai/k8sgpt/commit/067c3483e6b379bd710c7f799de63bc1890b6c81))
|
||||
* use-case while in cluster, connecting to an external ([#623](https://github.com/k8sgpt-ai/k8sgpt/issues/623)) ([1a7f45c](https://github.com/k8sgpt-ai/k8sgpt/commit/1a7f45cc55348d567148d01e61c7527e4d534f34))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** bump github.com/cyphar/filepath-securejoin ([#644](https://github.com/k8sgpt-ai/k8sgpt/issues/644)) ([25890e6](https://github.com/k8sgpt-ai/k8sgpt/commit/25890e6e3807171e655fec0d2081cedad3ad6273))
|
||||
* **deps:** update actions/checkout action to v4 ([#628](https://github.com/k8sgpt-ai/k8sgpt/issues/628)) ([e65d9a6](https://github.com/k8sgpt-ai/k8sgpt/commit/e65d9a650522120d602b2a62703aa2b39abfdea1))
|
||||
* **deps:** update actions/checkout digest to f43a0e5 ([#612](https://github.com/k8sgpt-ai/k8sgpt/issues/612)) ([6f9f7b2](https://github.com/k8sgpt-ai/k8sgpt/commit/6f9f7b2b602605f3be7fd02bd521574e9c26fa78))
|
||||
* **deps:** update docker/build-push-action action to v5 ([#643](https://github.com/k8sgpt-ai/k8sgpt/issues/643)) ([241f1bd](https://github.com/k8sgpt-ai/k8sgpt/commit/241f1bd6dfcb772711551aac42e48a2f59e64046))
|
||||
* **deps:** update docker/login-action action to v3 ([#648](https://github.com/k8sgpt-ai/k8sgpt/issues/648)) ([b491c92](https://github.com/k8sgpt-ai/k8sgpt/commit/b491c9200e781284737dd74a9789dfc0c1e7b14a))
|
||||
* **deps:** update docker/setup-buildx-action action to v3 ([#649](https://github.com/k8sgpt-ai/k8sgpt/issues/649)) ([598ef22](https://github.com/k8sgpt-ai/k8sgpt/commit/598ef22e570c1db678d583638c83e242f3b313d7))
|
||||
* **deps:** update docker/setup-buildx-action digest to 885d146 ([#615](https://github.com/k8sgpt-ai/k8sgpt/issues/615)) ([2c81dad](https://github.com/k8sgpt-ai/k8sgpt/commit/2c81dadb4d4abcdc3608be768c1f3aae87e53a68))
|
||||
* **deps:** update goreleaser/goreleaser-action action to v5 ([#641](https://github.com/k8sgpt-ai/k8sgpt/issues/641)) ([00d7a27](https://github.com/k8sgpt-ai/k8sgpt/commit/00d7a27ec1ea1bd49ab1879b8ffa0b9e7c0b6adf))
|
||||
* **deps:** update goreleaser/goreleaser-action digest to 5fdedb9 ([#631](https://github.com/k8sgpt-ai/k8sgpt/issues/631)) ([5de3b64](https://github.com/k8sgpt-ai/k8sgpt/commit/5de3b640988783df5a04db368f79b9b9eefdb8bf))
|
||||
* fixes a bug where filters do not deactive ([#621](https://github.com/k8sgpt-ai/k8sgpt/issues/621)) ([133850f](https://github.com/k8sgpt-ai/k8sgpt/commit/133850f984cc0bb41ec1e4521a32ab30558778f1))
|
||||
* slice loop replace ([#627](https://github.com/k8sgpt-ai/k8sgpt/issues/627)) ([c24825b](https://github.com/k8sgpt-ai/k8sgpt/commit/c24825b81025c5cd79224a79b52d6c5efdc00511))
|
||||
* updated protobuf libs ([#614](https://github.com/k8sgpt-ai/k8sgpt/issues/614)) ([5e17e66](https://github.com/k8sgpt-ai/k8sgpt/commit/5e17e666659c0eb057562def70d491daa995e5a2))
|
||||
* updated schema for integrations support ([#616](https://github.com/k8sgpt-ai/k8sgpt/issues/616)) ([8f0a2fd](https://github.com/k8sgpt-ai/k8sgpt/commit/8f0a2fd41d6705da4d1a1d288f3b6ce19711f30d))
|
||||
|
||||
## [0.3.14](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.13...v0.3.14) (2023-08-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* configauditreport ([#609](https://github.com/k8sgpt-ai/k8sgpt/issues/609)) ([44d3613](https://github.com/k8sgpt-ai/k8sgpt/commit/44d3613c1f950837c6b112ddde0dc3e90f73dc1b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update kubernetes packages to v0.27.4 ([#565](https://github.com/k8sgpt-ai/k8sgpt/issues/565)) ([3cc7aa5](https://github.com/k8sgpt-ai/k8sgpt/commit/3cc7aa56d8efc6e78badf3be1cb3d5726074156e))
|
||||
* **deps:** update module github.com/aquasecurity/trivy-operator to v0.15.1 ([#576](https://github.com/k8sgpt-ai/k8sgpt/issues/576)) ([c364074](https://github.com/k8sgpt-ai/k8sgpt/commit/c3640744c5cbf036321a14b90c1fdefa17c5321d))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.304 ([#558](https://github.com/k8sgpt-ai/k8sgpt/issues/558)) ([cf9069e](https://github.com/k8sgpt-ai/k8sgpt/commit/cf9069ef572fea9a947d7de5b0c0e44f34620a69))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.307 ([#574](https://github.com/k8sgpt-ai/k8sgpt/issues/574)) ([8ae91ec](https://github.com/k8sgpt-ai/k8sgpt/commit/8ae91ec744d1fead3b0aa570c904e9e3ad5ab5ef))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.308 ([#579](https://github.com/k8sgpt-ai/k8sgpt/issues/579)) ([7e8668a](https://github.com/k8sgpt-ai/k8sgpt/commit/7e8668a56bb25b7da3957cf4c05847d022825c10))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.309 ([#584](https://github.com/k8sgpt-ai/k8sgpt/issues/584)) ([227e1cd](https://github.com/k8sgpt-ai/k8sgpt/commit/227e1cd69f38654126750902a89408643bdb30fb))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.312 ([#586](https://github.com/k8sgpt-ai/k8sgpt/issues/586)) ([aafac93](https://github.com/k8sgpt-ai/k8sgpt/commit/aafac9345fbab16b1fe23ea76d6c1c362c44c080))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.313 ([#587](https://github.com/k8sgpt-ai/k8sgpt/issues/587)) ([f1479ba](https://github.com/k8sgpt-ai/k8sgpt/commit/f1479babbaaf6770d4a106d80f22b2ffb736cbad))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.315 ([#588](https://github.com/k8sgpt-ai/k8sgpt/issues/588)) ([fe29361](https://github.com/k8sgpt-ai/k8sgpt/commit/fe29361e335f3d186dc3d7651823e9bb03649652))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.317 ([#591](https://github.com/k8sgpt-ai/k8sgpt/issues/591)) ([9802e82](https://github.com/k8sgpt-ai/k8sgpt/commit/9802e82ff54bc55b670e25f75c69a29a985c21ae))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.327 ([#597](https://github.com/k8sgpt-ai/k8sgpt/issues/597)) ([aee83b7](https://github.com/k8sgpt-ai/k8sgpt/commit/aee83b74b20117f136876ec426318914aee8c4d1))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.329 ([#610](https://github.com/k8sgpt-ai/k8sgpt/issues/610)) ([0e5be89](https://github.com/k8sgpt-ai/k8sgpt/commit/0e5be89e5ccb70e9e9a44ad70f161c7b344d04f2))
|
||||
* **deps:** update module github.com/mittwald/go-helm-client to v0.12.3 ([#582](https://github.com/k8sgpt-ai/k8sgpt/issues/582)) ([c2770f3](https://github.com/k8sgpt-ai/k8sgpt/commit/c2770f38a6f0d3248747927155505db505f5e960))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.14.1 ([#573](https://github.com/k8sgpt-ai/k8sgpt/issues/573)) ([b52424a](https://github.com/k8sgpt-ai/k8sgpt/commit/b52424a9b1a554739cb8e08e296045c181d4041c))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.14.2 ([#603](https://github.com/k8sgpt-ai/k8sgpt/issues/603)) ([81fcf8b](https://github.com/k8sgpt-ai/k8sgpt/commit/81fcf8b5d46387eca7128b877c0652fdf4ed999c))
|
||||
* **deps:** update module go.uber.org/zap to v1.25.0 ([#589](https://github.com/k8sgpt-ai/k8sgpt/issues/589)) ([9672cea](https://github.com/k8sgpt-ai/k8sgpt/commit/9672cea228de976772f453e6a05ce05456741de8))
|
||||
* **deps:** update module golang.org/x/term to v0.11.0 ([#593](https://github.com/k8sgpt-ai/k8sgpt/issues/593)) ([7f109cd](https://github.com/k8sgpt-ai/k8sgpt/commit/7f109cdcfac00a329a53121287e44c2567af6b4a))
|
||||
* **deps:** update module google.golang.org/grpc to v1.57.0 ([#585](https://github.com/k8sgpt-ai/k8sgpt/issues/585)) ([59897f3](https://github.com/k8sgpt-ai/k8sgpt/commit/59897f330a037f1e5de0f958dd93b826e2ce481d))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.12.3 ([#602](https://github.com/k8sgpt-ai/k8sgpt/issues/602)) ([7910c9a](https://github.com/k8sgpt-ai/k8sgpt/commit/7910c9aa2c40f3c1837cce179dd1fc91a9744946))
|
||||
* optimize analyze service ([#461](https://github.com/k8sgpt-ai/k8sgpt/issues/461)) ([cc665ea](https://github.com/k8sgpt-ai/k8sgpt/commit/cc665ea4f3f279c30c7dd7996786e6bdce88acc8))
|
||||
* use kubeconfig file when user specify it ([#605](https://github.com/k8sgpt-ai/k8sgpt/issues/605)) ([e3b21ec](https://github.com/k8sgpt-ai/k8sgpt/commit/e3b21ec5ecd5f823470c2c2f570ed89a2c071b5a)), closes [#604](https://github.com/k8sgpt-ai/k8sgpt/issues/604)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** exclude retracted cohere-go versions ([#583](https://github.com/k8sgpt-ai/k8sgpt/issues/583)) ([f8a53a5](https://github.com/k8sgpt-ai/k8sgpt/commit/f8a53a5c035fd3e3598666d9792c4e1231f9838d))
|
||||
* **deps:** update actions/setup-go digest to 93397be ([#600](https://github.com/k8sgpt-ai/k8sgpt/issues/600)) ([1a0ae1a](https://github.com/k8sgpt-ai/k8sgpt/commit/1a0ae1a086d328b1eaa70c412122427a6e8df2f5))
|
||||
* **deps:** update google-github-actions/release-please-action digest to ca6063f ([#572](https://github.com/k8sgpt-ai/k8sgpt/issues/572)) ([fba1a8e](https://github.com/k8sgpt-ai/k8sgpt/commit/fba1a8ed8c7cc2f7b0aace246f8797ea6c27e455))
|
||||
* **deps:** update goreleaser/goreleaser-action digest to 3fa32b8 ([#601](https://github.com/k8sgpt-ai/k8sgpt/issues/601)) ([610720a](https://github.com/k8sgpt-ai/k8sgpt/commit/610720a95c9d5eb49c77e7a929cd766a04e534a4))
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to 951dc8b ([#594](https://github.com/k8sgpt-ai/k8sgpt/issues/594)) ([9acaec0](https://github.com/k8sgpt-ai/k8sgpt/commit/9acaec00c4d084c4ec3e40e4a6a8b0136dcc4aa1))
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to f17c2e2 ([#598](https://github.com/k8sgpt-ai/k8sgpt/issues/598)) ([2251321](https://github.com/k8sgpt-ai/k8sgpt/commit/22513216960f06d572ec53480e290b1f4e5ff1d8))
|
||||
* upgraded cohere backend ([#580](https://github.com/k8sgpt-ai/k8sgpt/issues/580)) ([43b0d70](https://github.com/k8sgpt-ai/k8sgpt/commit/43b0d707e7eac326594f5f6c7ab4c885772846d2))
|
||||
|
||||
## [0.3.13](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.12...v0.3.13) (2023-07-20)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* continue on absent service ([#569](https://github.com/k8sgpt-ai/k8sgpt/issues/569)) ([153d38d](https://github.com/k8sgpt-ai/k8sgpt/commit/153d38deb060cb84d606f8391e5700025ce02a9b))
|
||||
|
||||
## [0.3.12](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.11...v0.3.12) (2023-07-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Cohere backend ([#563](https://github.com/k8sgpt-ai/k8sgpt/issues/563)) ([781ecb7](https://github.com/k8sgpt-ai/k8sgpt/commit/781ecb7aad689e6709678c9690c112115e3cf6c7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.300 ([#554](https://github.com/k8sgpt-ai/k8sgpt/issues/554)) ([dc46333](https://github.com/k8sgpt-ai/k8sgpt/commit/dc463334bccdf16106cff4e688a83bf0984d6e27))
|
||||
* **deps:** update module github.com/mittwald/go-helm-client to v0.12.2 ([#562](https://github.com/k8sgpt-ai/k8sgpt/issues/562)) ([2e0db55](https://github.com/k8sgpt-ai/k8sgpt/commit/2e0db553f92b5ca691b5957b180be35131ab4e2f))
|
||||
* **deps:** update module google.golang.org/grpc to v1.56.2 ([#546](https://github.com/k8sgpt-ai/k8sgpt/issues/546)) ([cc83fe1](https://github.com/k8sgpt-ai/k8sgpt/commit/cc83fe19bafc87647fa0293189f90c84d2dd8edb))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.12.2 ([#555](https://github.com/k8sgpt-ai/k8sgpt/issues/555)) ([9eb96c4](https://github.com/k8sgpt-ai/k8sgpt/commit/9eb96c495cdb1247b664de625a036902b5e156ff))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* fixing edge cases with missing wh service ([#561](https://github.com/k8sgpt-ai/k8sgpt/issues/561)) ([c422215](https://github.com/k8sgpt-ai/k8sgpt/commit/c42221512bfdab7ac792963d459bf9f8dac3954c))
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
* fix readme for anonymization ([#559](https://github.com/k8sgpt-ai/k8sgpt/issues/559)) ([70bec05](https://github.com/k8sgpt-ai/k8sgpt/commit/70bec050d854be6f559065278d6a583d8e0e333b))
|
||||
|
||||
## [0.3.11](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.10...v0.3.11) (2023-07-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* admission webhooks ([#553](https://github.com/k8sgpt-ai/k8sgpt/issues/553)) ([06e8532](https://github.com/k8sgpt-ai/k8sgpt/commit/06e8532f88616a988a4e41ed8cdac62cf0f243a5))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update docker/setup-buildx-action digest to 4c0219f ([#547](https://github.com/k8sgpt-ai/k8sgpt/issues/547)) ([1a3f299](https://github.com/k8sgpt-ai/k8sgpt/commit/1a3f2992108e857f8c8c07eff16599d00b50110e))
|
||||
|
||||
## [0.3.10](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.9...v0.3.10) (2023-07-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Validating/Mutating webhook analyzer ([#548](https://github.com/k8sgpt-ai/k8sgpt/issues/548)) ([750a10d](https://github.com/k8sgpt-ai/k8sgpt/commit/750a10d44c59bc90de5241d1128ee74fa38bf350))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.298 ([#545](https://github.com/k8sgpt-ai/k8sgpt/issues/545)) ([d1096dc](https://github.com/k8sgpt-ai/k8sgpt/commit/d1096dc31a692013f40980649e5cc2d402869ceb))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.299 ([#549](https://github.com/k8sgpt-ai/k8sgpt/issues/549)) ([ecd7790](https://github.com/k8sgpt-ai/k8sgpt/commit/ecd7790efe2ca88259451761202c90cb842ff04b))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.13.0 ([#399](https://github.com/k8sgpt-ai/k8sgpt/issues/399)) ([21df094](https://github.com/k8sgpt-ai/k8sgpt/commit/21df094bda31a14235fb2244e8cef74d3c92d919))
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.14.0 ([#550](https://github.com/k8sgpt-ai/k8sgpt/issues/550)) ([9dcab94](https://github.com/k8sgpt-ai/k8sgpt/commit/9dcab945460e5972f895fa5302e3425750d635c7))
|
||||
* **deps:** update module golang.org/x/term to v0.10.0 ([#542](https://github.com/k8sgpt-ai/k8sgpt/issues/542)) ([1276b3e](https://github.com/k8sgpt-ai/k8sgpt/commit/1276b3e89715b1cfb553e60d4f25592acef80a6f))
|
||||
|
||||
## [0.3.9](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.8...v0.3.9) (2023-07-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* details flag to list command ([#537](https://github.com/k8sgpt-ai/k8sgpt/issues/537)) ([2309b0d](https://github.com/k8sgpt-ai/k8sgpt/commit/2309b0dfe20e27b6afe283a6be21ad7a0652ac99))
|
||||
* upgrading azure client impl ([#526](https://github.com/k8sgpt-ai/k8sgpt/issues/526)) ([367fe8f](https://github.com/k8sgpt-ai/k8sgpt/commit/367fe8f74c6a9e26f0d9c3b25a86093530fb85b2))
|
||||
* upgrading the proto files to fix user issues ([#515](https://github.com/k8sgpt-ai/k8sgpt/issues/515)) ([c88fc88](https://github.com/k8sgpt-ai/k8sgpt/commit/c88fc889e4f6089e48f37d90e349d5c61ea0b952))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 'intergration' typos ([#508](https://github.com/k8sgpt-ai/k8sgpt/issues/508)) ([64b93c9](https://github.com/k8sgpt-ai/k8sgpt/commit/64b93c9116b6a7f82419f1c4fff98fa68b8c0aca))
|
||||
* add --no-install for activate command ([#536](https://github.com/k8sgpt-ai/k8sgpt/issues/536)) ([1f5462c](https://github.com/k8sgpt-ai/k8sgpt/commit/1f5462c80bd04f63c2b55889c987634251635812))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20230620082254-6f80f9533908.1 ([#516](https://github.com/k8sgpt-ai/k8sgpt/issues/516)) ([06e50d5](https://github.com/k8sgpt-ai/k8sgpt/commit/06e50d57db3aa2e5a68b093e2ba25c0e33dc6343))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.284 ([#501](https://github.com/k8sgpt-ai/k8sgpt/issues/501)) ([d87127a](https://github.com/k8sgpt-ai/k8sgpt/commit/d87127a309734847a56bf95c2e947e2270f94a88))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.286 ([#514](https://github.com/k8sgpt-ai/k8sgpt/issues/514)) ([b9cf522](https://github.com/k8sgpt-ai/k8sgpt/commit/b9cf5226853619655e98f2156bfd0b8513511bb3))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.288 ([#519](https://github.com/k8sgpt-ai/k8sgpt/issues/519)) ([57695b4](https://github.com/k8sgpt-ai/k8sgpt/commit/57695b44b6429319860a76e4e02016dafe3ed0b0))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.289 ([#524](https://github.com/k8sgpt-ai/k8sgpt/issues/524)) ([fafb695](https://github.com/k8sgpt-ai/k8sgpt/commit/fafb69544f4edda670bad6973332a20a7f0f055e))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.292 ([#530](https://github.com/k8sgpt-ai/k8sgpt/issues/530)) ([566f752](https://github.com/k8sgpt-ai/k8sgpt/commit/566f7525eef9f65dd2ab6a47bd0012bfb91e2a12))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.294 ([#535](https://github.com/k8sgpt-ai/k8sgpt/issues/535)) ([3067fa9](https://github.com/k8sgpt-ai/k8sgpt/commit/3067fa98f4a8990c9a930e53ad93f89cf35e0d62))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.295 ([#540](https://github.com/k8sgpt-ai/k8sgpt/issues/540)) ([767e4cb](https://github.com/k8sgpt-ai/k8sgpt/commit/767e4cbc4127e2017a3a9c4b182ccc833debc6a5))
|
||||
* **deps:** update module github.com/prometheus/client_golang to v1.16.0 ([#507](https://github.com/k8sgpt-ai/k8sgpt/issues/507)) ([14e5691](https://github.com/k8sgpt-ai/k8sgpt/commit/14e5691190bf772c05477cbcb811ed71bec450a3))
|
||||
* **deps:** update module google.golang.org/grpc to v1.56.0 ([#510](https://github.com/k8sgpt-ai/k8sgpt/issues/510)) ([f3e0b9b](https://github.com/k8sgpt-ai/k8sgpt/commit/f3e0b9b56d13397c79f57e76bdd6b741bb565fb4))
|
||||
* **deps:** update module google.golang.org/grpc to v1.56.1 ([#520](https://github.com/k8sgpt-ai/k8sgpt/issues/520)) ([be52308](https://github.com/k8sgpt-ai/k8sgpt/commit/be52308c99f6aed73e2c20d260823795d45876f5))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.12.1 ([#503](https://github.com/k8sgpt-ai/k8sgpt/issues/503)) ([0f03ddc](https://github.com/k8sgpt-ai/k8sgpt/commit/0f03ddcf0f5ec79bc6dbb74c654e0d8fac634a0a))
|
||||
* displaying correct yaml config location on app start ([#521](https://github.com/k8sgpt-ai/k8sgpt/issues/521)) ([b7d4602](https://github.com/k8sgpt-ai/k8sgpt/commit/b7d4602cb8aaaa0c22a0a5941d8c6edad7c58db4))
|
||||
* remove provider from default on delete ([#529](https://github.com/k8sgpt-ai/k8sgpt/issues/529)) ([5a983c4](https://github.com/k8sgpt-ai/k8sgpt/commit/5a983c4a0a511389e25cffe12999b903b85cd96d))
|
||||
* typo in add command ([#539](https://github.com/k8sgpt-ai/k8sgpt/issues/539)) ([da750df](https://github.com/k8sgpt-ai/k8sgpt/commit/da750df16bde878f7619aa58ef5e7ef7d3173b2b))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update anchore/sbom-action action to v0.14.3 ([#517](https://github.com/k8sgpt-ai/k8sgpt/issues/517)) ([0521060](https://github.com/k8sgpt-ai/k8sgpt/commit/05210604109a6e892bb465df11038b8c24d68076))
|
||||
* **deps:** update docker/build-push-action digest to 2eb1c19 ([#499](https://github.com/k8sgpt-ai/k8sgpt/issues/499)) ([9df75cc](https://github.com/k8sgpt-ai/k8sgpt/commit/9df75cc959f7ed23cae8e3761498ea6c56885788))
|
||||
* **deps:** update docker/login-action digest to 465a078 ([#488](https://github.com/k8sgpt-ai/k8sgpt/issues/488)) ([c15a561](https://github.com/k8sgpt-ai/k8sgpt/commit/c15a561b635dc678bb8de15c6623914942475537))
|
||||
* **deps:** update docker/setup-buildx-action digest to 16c0bc4 ([#532](https://github.com/k8sgpt-ai/k8sgpt/issues/532)) ([5662d59](https://github.com/k8sgpt-ai/k8sgpt/commit/5662d5932ff3beb8c1a31fc2088c5e703e90ec79))
|
||||
* **deps:** update docker/setup-buildx-action digest to ecf9528 ([#498](https://github.com/k8sgpt-ai/k8sgpt/issues/498)) ([f4d7876](https://github.com/k8sgpt-ai/k8sgpt/commit/f4d78768388774f62d87acd89e71689535e538f7))
|
||||
* **deps:** update google-github-actions/release-please-action digest to 8016a66 ([#523](https://github.com/k8sgpt-ai/k8sgpt/issues/523)) ([d56861d](https://github.com/k8sgpt-ai/k8sgpt/commit/d56861d4bad475da09992813fc256a0d99399eab))
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to 22adb9d ([#525](https://github.com/k8sgpt-ai/k8sgpt/issues/525)) ([3146754](https://github.com/k8sgpt-ai/k8sgpt/commit/314675477917063dcfb847880fb3186f8bdf32f6))
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to 994abff ([#513](https://github.com/k8sgpt-ai/k8sgpt/issues/513)) ([1819c3b](https://github.com/k8sgpt-ai/k8sgpt/commit/1819c3bf1512291cd637c115e8b82c9e0e8885a0))
|
||||
* sorting out the dependency hell ([#518](https://github.com/k8sgpt-ai/k8sgpt/issues/518)) ([cd7807a](https://github.com/k8sgpt-ai/k8sgpt/commit/cd7807a48481f298422d9b1c8066b431fad3ae5a))
|
||||
|
||||
## [0.3.8](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.7...v0.3.8) (2023-06-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* fix for s3 cache from operator ([f6db6ce](https://github.com/k8sgpt-ai/k8sgpt/commit/f6db6ce86163dcb4b5ec4bd99b8a3842dd0c60bb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update kubernetes packages to v0.27.3 ([#504](https://github.com/k8sgpt-ai/k8sgpt/issues/504)) ([b1c6ec3](https://github.com/k8sgpt-ai/k8sgpt/commit/b1c6ec3c0919649bc551ec0047a1d9c2420f4264))
|
||||
|
||||
## [0.3.7](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.6...v0.3.7) (2023-06-13)
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ We're happy that you want to contribute to this project. Please read the section
|
||||
- Assign yourself to the issue, if you are working on it (if you are not a member of the organization, please leave a comment on the issue)
|
||||
- Make your changes
|
||||
- Keep pull requests small and focused, if you have multiple changes, please open multiple PRs
|
||||
- Create a pull request back to the upstream repository and follow follow the [pull request template](.github/pull_request_template.md) guidelines.
|
||||
- Create a pull request back to the upstream repository and follow the [pull request template](.github/pull_request_template.md) guidelines.
|
||||
- Wait for a review and address any comments
|
||||
|
||||
**Opening PRs**
|
||||
|
||||
241
README.md
241
README.md
@@ -8,12 +8,19 @@
|
||||

|
||||

|
||||
[](https://bestpractices.coreinfrastructure.org/projects/7272)
|
||||
[](https://docs.k8sgpt.ai/)
|
||||
[](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, 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>
|
||||
|
||||
<img src="images/demo4.gif" width=650px; />
|
||||
@@ -34,7 +41,7 @@ brew install k8sgpt
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.7/k8sgpt_386.rpm
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.30/k8sgpt_386.rpm
|
||||
sudo rpm -ivh k8sgpt_386.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -43,7 +50,7 @@ brew install k8sgpt
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.7/k8sgpt_amd64.rpm
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.30/k8sgpt_amd64.rpm
|
||||
sudo rpm -ivh -i k8sgpt_amd64.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -55,7 +62,7 @@ brew install k8sgpt
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.7/k8sgpt_386.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.30/k8sgpt_386.deb
|
||||
sudo dpkg -i k8sgpt_386.deb
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -63,7 +70,7 @@ brew install k8sgpt
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.7/k8sgpt_amd64.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.30/k8sgpt_amd64.deb
|
||||
sudo dpkg -i k8sgpt_amd64.deb
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -76,14 +83,14 @@ brew install k8sgpt
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.7/k8sgpt_386.apk
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.30/k8sgpt_386.apk
|
||||
apk add k8sgpt_386.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
**64 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.7/k8sgpt_amd64.apk
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.30/k8sgpt_amd64.apk
|
||||
apk add k8sgpt_amd64.apk
|
||||
```
|
||||
<!---x-release-please-end-->x
|
||||
@@ -121,14 +128,14 @@ _This mode of operation is ideal for continuous monitoring of your cluster and c
|
||||
|
||||
## Quick Start
|
||||
|
||||
* Currently the default AI provider is OpenAI, you will need to generate an API key from [OpenAI](https://openai.com)
|
||||
* 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 documention from kubernetes.
|
||||
* You also run `k8sgpt analyze --with-doc` (with or without the explain flag) to get the official documentation from Kubernetes.
|
||||
|
||||
## Analyzers
|
||||
|
||||
@@ -149,12 +156,17 @@ you will be able to write your own analyzers.
|
||||
- [x] deploymentAnalyzer
|
||||
- [x] cronJobAnalyzer
|
||||
- [x] nodeAnalyzer
|
||||
- [x] mutatingWebhookAnalyzer
|
||||
- [x] validatingWebhookAnalyzer
|
||||
|
||||
#### Optional
|
||||
|
||||
- [x] hpaAnalyzer
|
||||
- [x] pdbAnalyzer
|
||||
- [x] networkPolicyAnalyzer
|
||||
- [x] gatewayClass
|
||||
- [x] gateway
|
||||
- [x] httproute
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -242,7 +254,7 @@ k8sgpt auth update $MY_BACKEND1,$MY_BACKEND2..
|
||||
_Remove configured backends_
|
||||
|
||||
```
|
||||
k8sgpt auth remove $MY_BACKEND1,$MY_BACKEND2..
|
||||
k8sgpt auth remove -b $MY_BACKEND1,$MY_BACKEND2..
|
||||
```
|
||||
|
||||
_List integrations_
|
||||
@@ -278,84 +290,34 @@ k8sgpt serve
|
||||
_Analysis with serve mode_
|
||||
|
||||
```
|
||||
curl -X GET "http://localhost:8080/analyze?namespace=k8sgpt&explain=false"
|
||||
grpcurl -plaintext -d '{"namespace": "k8sgpt", "explain": false}' localhost:8080 schema.v1.ServerService/Analyze
|
||||
```
|
||||
</details>
|
||||
|
||||
## LLM AI Backends
|
||||
|
||||
## Key Features
|
||||
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).
|
||||
|
||||
<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:
|
||||
You can list available providers using `k8sgpt auth list`:
|
||||
|
||||
```
|
||||
k8sgpt auth add --backend localai --model <model_name> --baseurl http://localhost:8080/v1
|
||||
```
|
||||
|
||||
Now you can analyze with the `localai` backend:
|
||||
|
||||
```
|
||||
k8sgpt analyze --explain --backend localai
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> AzureOpenAI provider </summary>
|
||||
|
||||
<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.
|
||||
|
||||
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>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:
|
||||
Default:
|
||||
> openai
|
||||
Active:
|
||||
Active:
|
||||
Unused:
|
||||
> openai
|
||||
> azureopenai
|
||||
Unused:
|
||||
> localai
|
||||
> azureopenai
|
||||
> cohere
|
||||
> amazonbedrock
|
||||
> amazonsagemaker
|
||||
> google
|
||||
> huggingface
|
||||
> noopai
|
||||
|
||||
> googlevertexai
|
||||
```
|
||||
|
||||
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_
|
||||
|
||||
@@ -364,16 +326,14 @@ 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.
|
||||
@@ -394,12 +354,61 @@ The Kubernetes system is trying to scale a StatefulSet named tGLcCRcHa1Ce5Rs usi
|
||||
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.
|
||||
```
|
||||
|
||||
Note: **Anonymization does not currently apply to events.**
|
||||
|
||||
### Further Details
|
||||
|
||||
**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**.*
|
||||
|
||||
- The following is the list of analysers in which data is **being masked**:-
|
||||
|
||||
- Statefulset
|
||||
- Service
|
||||
- PodDisruptionBudget
|
||||
- Node
|
||||
- NetworkPolicy
|
||||
- Ingress
|
||||
- HPA
|
||||
- Deployment
|
||||
- Cronjob
|
||||
|
||||
- The following is the list of analysers in which data is **not being masked**:-
|
||||
|
||||
- RepicaSet
|
||||
- PersistentVolumeClaim
|
||||
- Pod
|
||||
- **_*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._
|
||||
|
||||
- The following is the list of fields which are not **being masked**:-
|
||||
|
||||
- Describe
|
||||
- ObjectStatus
|
||||
- Replicas
|
||||
- ContainerStatus
|
||||
- **_*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))_.
|
||||
|
||||
### 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.
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> Configuration management</summary>
|
||||
|
||||
`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:
|
||||
@@ -411,31 +420,75 @@ Config file locations:
|
||||
</details>
|
||||
|
||||
<details>
|
||||
There may be scenarios where caching remotely is prefered.
|
||||
In these scenarios K8sGPT supports AWS S3 Integration.
|
||||
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>
|
||||
|
||||
_As a prerequisite `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are required as environmental variables._
|
||||
<summary> Remote caching </summary>
|
||||
<em>Note: You can only configure and use only one remote cache at a time</em>
|
||||
|
||||
_Adding a remote cache_
|
||||
Note: this will create the bucket if it does not exist
|
||||
```
|
||||
k8sgpt cache add --region <aws region> --bucket <name>
|
||||
```
|
||||
|
||||
* 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
|
||||
```
|
||||
|
||||
_Removing the remote cache_
|
||||
Note: this will not delete the bucket
|
||||
_Purging an object from the cache_
|
||||
Note: purging an object using this command will delete upstream files, so it requires appropriate permissions.
|
||||
```
|
||||
k8sgpt cache remove --bucket <name>
|
||||
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_
|
||||
|
||||
</details>
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -446,8 +499,12 @@ Find our official documentation available [here](https://docs.k8sgpt.ai)
|
||||
|
||||
Please read our [contributing guide](./CONTRIBUTING.md).
|
||||
## Community
|
||||
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-1rwe5fpzq-VNtJK8DmYbbm~iWL1H34nw)
|
||||
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)
|
||||
|
||||
@@ -8,4 +8,4 @@ For example if there is a vulnerability in release `0.1.0` we will fix that rele
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you are aware of a vulnverability please feel free to disclose it responsibly to contact@k8sgpt.ai or to one of our maintainers in our Slack community.
|
||||
If you are aware of a vulnerability please feel free to disclose it responsibly to contact@k8sgpt.ai or to one of our maintainers in our Slack community.
|
||||
|
||||
@@ -16,23 +16,28 @@ 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
|
||||
anonymize bool
|
||||
maxConcurrency int
|
||||
withDoc bool
|
||||
interactiveMode bool
|
||||
customAnalysis bool
|
||||
)
|
||||
|
||||
// AnalyzeCmd represents the problems command
|
||||
@@ -43,37 +48,72 @@ 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,
|
||||
nocache,
|
||||
explain,
|
||||
maxConcurrency,
|
||||
withDoc,
|
||||
interactiveMode,
|
||||
)
|
||||
|
||||
// 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))
|
||||
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 +125,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 +134,9 @@ 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")
|
||||
|
||||
}
|
||||
|
||||
@@ -26,16 +26,28 @@ import (
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBackend = "openai"
|
||||
defaultModel = "gpt-3.5-turbo"
|
||||
)
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Configure new provider",
|
||||
Long: "The new command allows to configure a new backend AI provider",
|
||||
Short: "Add new provider",
|
||||
Long: "The add command allows to configure a new backend AI provider",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
backend, _ := cmd.Flags().GetString("backend")
|
||||
if strings.ToLower(backend) == "azureopenai" {
|
||||
_ = cmd.MarkFlagRequired("engine")
|
||||
_ = cmd.MarkFlagRequired("baseurl")
|
||||
}
|
||||
if strings.ToLower(backend) == "amazonsagemaker" {
|
||||
_ = cmd.MarkFlagRequired("endpointname")
|
||||
_ = cmd.MarkFlagRequired("providerRegion")
|
||||
}
|
||||
if strings.ToLower(backend) == "amazonbedrock" {
|
||||
_ = cmd.MarkFlagRequired("providerRegion")
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
@@ -65,14 +77,27 @@ var addCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// check if backend is not empty and a valid value
|
||||
if backend == "" || !validBackend(ai.Backends, backend) {
|
||||
color.Red("Error: Backend AI cannot be empty and accepted values are '%v'", strings.Join(ai.Backends, ", "))
|
||||
os.Exit(1)
|
||||
if backend == "" {
|
||||
color.Yellow(fmt.Sprintf("Warning: backend input is empty, will use the default value: %s", defaultBackend))
|
||||
backend = defaultBackend
|
||||
} else {
|
||||
if !validBackend(ai.Backends, backend) {
|
||||
color.Red("Error: Backend AI accepted values are '%v'", strings.Join(ai.Backends, ", "))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// check if model is not empty
|
||||
if model == "" {
|
||||
color.Red("Error: Model cannot be empty.")
|
||||
model = defaultModel
|
||||
color.Yellow(fmt.Sprintf("Warning: model input is empty, will use the default value: %s", defaultModel))
|
||||
}
|
||||
if temperature > 1.0 || temperature < 0.0 {
|
||||
color.Red("Error: temperature ranges from 0 to 1.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if topP > 1.0 || topP < 0.0 {
|
||||
color.Red("Error: topP ranges from 0 to 1.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -89,11 +114,17 @@ var addCmd = &cobra.Command{
|
||||
|
||||
// create new provider object
|
||||
newProvider := ai.AIProvider{
|
||||
Name: backend,
|
||||
Model: model,
|
||||
Password: password,
|
||||
BaseURL: baseURL,
|
||||
Engine: engine,
|
||||
Name: backend,
|
||||
Model: model,
|
||||
Password: password,
|
||||
BaseURL: baseURL,
|
||||
EndpointName: endpointName,
|
||||
Engine: engine,
|
||||
Temperature: temperature,
|
||||
ProviderRegion: providerRegion,
|
||||
ProviderId: providerId,
|
||||
TopP: topP,
|
||||
MaxTokens: maxTokens,
|
||||
}
|
||||
|
||||
if providerIndex == -1 {
|
||||
@@ -114,13 +145,25 @@ var addCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
// add flag for backend
|
||||
addCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
|
||||
addCmd.Flags().StringVarP(&backend, "backend", "b", defaultBackend, "Backend AI provider")
|
||||
// add flag for model
|
||||
addCmd.Flags().StringVarP(&model, "model", "m", "gpt-3.5-turbo", "Backend AI model")
|
||||
addCmd.Flags().StringVarP(&model, "model", "m", defaultModel, "Backend AI model")
|
||||
// add flag for password
|
||||
addCmd.Flags().StringVarP(&password, "password", "p", "", "Backend AI password")
|
||||
// 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` (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.")
|
||||
// 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 (only for amazonbedrock, googlevertexai backend)")
|
||||
//add flag for vertexAI Project ID
|
||||
addCmd.Flags().StringVarP(&providerId, "providerId", "i", "", "Provider specific ID for e.g. project (only for googlevertexai backend)")
|
||||
}
|
||||
|
||||
@@ -19,11 +19,17 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
backend string
|
||||
password string
|
||||
baseURL string
|
||||
model string
|
||||
engine string
|
||||
backend string
|
||||
password string
|
||||
baseURL string
|
||||
endpointName string
|
||||
model string
|
||||
engine string
|
||||
temperature float32
|
||||
providerRegion string
|
||||
providerId string
|
||||
topP float32
|
||||
maxTokens int
|
||||
)
|
||||
|
||||
var configAI ai.AIConfiguration
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var details bool
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured providers",
|
||||
@@ -55,6 +57,13 @@ var listCmd = &cobra.Command{
|
||||
}
|
||||
if providerExists {
|
||||
fmt.Printf("> %s\n", color.GreenString(aiBackend))
|
||||
if details {
|
||||
for _, provider := range configAI.Providers {
|
||||
if provider.Name == aiBackend {
|
||||
printDetails(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Print(color.YellowString("Unused: \n"))
|
||||
@@ -71,3 +80,19 @@ var listCmd = &cobra.Command{
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
listCmd.Flags().BoolVar(&details, "details", false, "Print active provider configuration details")
|
||||
}
|
||||
|
||||
func printDetails(provider ai.AIProvider) {
|
||||
if provider.Model != "" {
|
||||
fmt.Printf(" - Model: %s\n", provider.Model)
|
||||
}
|
||||
if provider.Engine != "" {
|
||||
fmt.Printf(" - Engine: %s\n", provider.Engine)
|
||||
}
|
||||
if provider.BaseURL != "" {
|
||||
fmt.Printf(" - BaseURL: %s\n", provider.BaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,39 +23,45 @@ import (
|
||||
)
|
||||
|
||||
var removeCmd = &cobra.Command{
|
||||
Use: "remove [backend(s)]",
|
||||
Short: "Remove a provider",
|
||||
Long: "The command to remove an AI backend provider",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Use: "remove",
|
||||
Short: "Remove provider(s)",
|
||||
Long: "The command to remove AI backend provider(s)",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
_ = cmd.MarkFlagRequired("backends")
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
inputBackends := strings.Split(args[0], ",")
|
||||
if backend == "" {
|
||||
color.Red("Error: backends must be set.")
|
||||
_ = cmd.Help()
|
||||
return
|
||||
}
|
||||
inputBackends := strings.Split(backend, ",")
|
||||
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(inputBackends) == 0 {
|
||||
color.Red("Error: backend must be set.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, b := range inputBackends {
|
||||
foundBackend := false
|
||||
for i, provider := range configAI.Providers {
|
||||
if b == provider.Name {
|
||||
foundBackend = true
|
||||
configAI.Providers = append(configAI.Providers[:i], configAI.Providers[i+1:]...)
|
||||
if configAI.DefaultProvider == b {
|
||||
configAI.DefaultProvider = "openai"
|
||||
}
|
||||
color.Green("%s deleted from the AI backend provider list", b)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBackend {
|
||||
color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", backend)
|
||||
color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", b)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
viper.Set("ai", configAI)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
color.Red("Error writing config file: %s", err.Error())
|
||||
@@ -64,3 +70,8 @@ var removeCmd = &cobra.Command{
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// add flag for backends
|
||||
removeCmd.Flags().StringVarP(&backend, "backends", "b", "", "Backend AI providers to remove (separated by a comma)")
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ var updateCmd = &cobra.Command{
|
||||
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
|
||||
@@ -74,6 +78,7 @@ var updateCmd = &cobra.Command{
|
||||
if engine != "" {
|
||||
configAI.Providers[i].Engine = engine
|
||||
}
|
||||
configAI.Providers[i].Temperature = temperature
|
||||
color.Green("%s updated in the AI backend provider list", b)
|
||||
}
|
||||
}
|
||||
@@ -101,6 +106,8 @@ func init() {
|
||||
updateCmd.Flags().StringVarP(&password, "password", "p", "", "Update backend AI password")
|
||||
// update flag for url
|
||||
updateCmd.Flags().StringVarP(&baseURL, "baseurl", "u", "", "Update URL AI provider, (e.g `http://localhost:8080/v1`)")
|
||||
// add flag for temperature
|
||||
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")
|
||||
}
|
||||
|
||||
45
cmd/cache/add.go
vendored
45
cmd/cache/add.go
vendored
@@ -25,18 +25,37 @@ import (
|
||||
|
||||
var (
|
||||
region string
|
||||
//nolint:unused
|
||||
bucketName string
|
||||
storageAccount string
|
||||
containerName string
|
||||
projectId string
|
||||
endpoint string
|
||||
insecure bool
|
||||
)
|
||||
|
||||
// addCmd represents the add command
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Use: "add [cache type]",
|
||||
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`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(color.YellowString("Adding remote S3 based cache"))
|
||||
err := cache.AddRemoteCache(bucketname, region)
|
||||
if len(args) == 0 {
|
||||
color.Red("Error: Please provide a value for cache types. Run k8sgpt cache add --help")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(color.YellowString("Adding remote based cache"))
|
||||
cacheType := args[0]
|
||||
remoteCache, err := cache.NewCacheProvider(cacheType, bucketName, region, endpoint, storageAccount, containerName, projectId, insecure)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = cache.AddRemoteCache(remoteCache)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
@@ -46,9 +65,19 @@ var addCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
CacheCmd.AddCommand(addCmd)
|
||||
addCmd.Flags().StringVarP(®ion, "region", "r", "", "The region to use for the cache")
|
||||
addCmd.Flags().StringVarP(&bucketname, "bucket", "b", "", "The name of the bucket to use for the cache")
|
||||
addCmd.MarkFlagRequired("bucket")
|
||||
addCmd.MarkFlagRequired("region")
|
||||
|
||||
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")
|
||||
addCmd.MarkFlagsRequiredTogether("storageacc", "container")
|
||||
// Tedious check to ensure we don't include arguments from different providers
|
||||
addCmd.MarkFlagsMutuallyExclusive("region", "storageacc")
|
||||
addCmd.MarkFlagsMutuallyExclusive("region", "container")
|
||||
addCmd.MarkFlagsMutuallyExclusive("bucket", "storageacc")
|
||||
addCmd.MarkFlagsMutuallyExclusive("bucket", "container")
|
||||
addCmd.MarkFlagsMutuallyExclusive("projectid", "storageacc")
|
||||
addCmd.MarkFlagsMutuallyExclusive("projectid", "container")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
24
cmd/cache/list.go
vendored
24
cmd/cache/list.go
vendored
@@ -16,9 +16,11 @@ package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -30,22 +32,32 @@ var listCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// load remote cache if it is configured
|
||||
remoteCacheEnabled, err := cache.RemoteCacheEnabled()
|
||||
c, err := cache.GetCacheConfiguration()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
c := cache.New(false, remoteCacheEnabled)
|
||||
// list the contents of the cache
|
||||
names, err := c.List()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
println(name)
|
||||
|
||||
var headers []string
|
||||
obj := cache.CacheObjectDetails{}
|
||||
objType := reflect.TypeOf(obj)
|
||||
for i := 0; i < objType.NumField(); i++ {
|
||||
field := objType.Field(i)
|
||||
headers = append(headers, field.Name)
|
||||
}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader(headers)
|
||||
|
||||
for _, v := range names {
|
||||
table.Append([]string{v.Name, v.UpdatedAt.String()})
|
||||
}
|
||||
table.Render()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
54
cmd/cache/purge.go
vendored
Normal file
54
cmd/cache/purge.go
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
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"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var purgeCmd = &cobra.Command{
|
||||
Use: "purge [object name]",
|
||||
Short: "Purge a remote cache",
|
||||
Long: "This command allows you to delete/purge one object from the cache",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
color.Red("Error: Please provide a value for object name. Run k8sgpt cache purge --help")
|
||||
os.Exit(1)
|
||||
}
|
||||
objectKey := args[0]
|
||||
fmt.Println(color.YellowString("Purging a remote cache."))
|
||||
c, err := cache.GetCacheConfiguration()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = c.Remove(objectKey)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(color.GreenString("Object deleted."))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
CacheCmd.AddCommand(purgeCmd)
|
||||
}
|
||||
2
cmd/cache/remove.go
vendored
2
cmd/cache/remove.go
vendored
@@ -29,7 +29,7 @@ var removeCmd = &cobra.Command{
|
||||
Long: `This command allows you to remove the remote cache and use the default filecache.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
err := cache.RemoveRemoteCache(bucketname)
|
||||
err := cache.RemoveRemoteCache()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -45,6 +45,13 @@ var addCmd = &cobra.Command{
|
||||
for _, filter := range availableFilters {
|
||||
if filter == f {
|
||||
foundFilter = true
|
||||
|
||||
// WARNING: This is to enable users correctly understand implications
|
||||
// of enabling logs
|
||||
if filter == "Log" {
|
||||
color.Yellow("Warning: by enabling logs, you will be sending potentially sensitive data to the AI backend.")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@ package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/integration"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
@@ -30,7 +32,7 @@ var listCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
coreFilters, additionalFilters, integrationFilters := analyzer.ListFilters()
|
||||
|
||||
integration := integration.NewIntegration()
|
||||
availableFilters := append(append(coreFilters, additionalFilters...), integrationFilters...)
|
||||
|
||||
if len(activeFilters) == 0 {
|
||||
@@ -39,12 +41,17 @@ 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
|
||||
if util.SliceContainsString(integrationFilters, filter) {
|
||||
// but if the integration is inactive, remove
|
||||
if slices.Contains(integrationFilters, filter) {
|
||||
fmt.Printf("> %s\n", color.BlueString("%s (integration)", filter))
|
||||
} else {
|
||||
fmt.Printf("> %s\n", color.GreenString(filter))
|
||||
// This strange bit of logic will loop through every integration via
|
||||
// OwnsAnalyzer subcommand to check the filter and as the integrationFilters...
|
||||
// was no match, we know this isn't part of an active integration
|
||||
if _, err := integration.AnalyzerByIntegration(filter); err != nil {
|
||||
fmt.Printf("> %s\n", color.GreenString(filter))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,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"
|
||||
@@ -85,6 +87,6 @@ func printInstructions(isGui bool, backendType string) {
|
||||
color.Green("Please open: https://beta.openai.com/account/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,6 +21,8 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var skipInstall bool
|
||||
|
||||
// activateCmd represents the activate command
|
||||
var activateCmd = &cobra.Command{
|
||||
Use: "activate [integration]",
|
||||
@@ -39,7 +41,7 @@ var activateCmd = &cobra.Command{
|
||||
|
||||
integration := integration.NewIntegration()
|
||||
// Check if the integation exists
|
||||
err := integration.Activate(integrationName, namespace, activeFilters)
|
||||
err := integration.Activate(integrationName, namespace, activeFilters, skipInstall)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
return
|
||||
@@ -51,5 +53,5 @@ 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)")
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ var (
|
||||
var IntegrationCmd = &cobra.Command{
|
||||
Use: "integration",
|
||||
Aliases: []string{"integrations"},
|
||||
Short: "Intergrate another tool into K8sGPT",
|
||||
Long: `Intergrate another tool into K8sGPT. For example:
|
||||
Short: "Integrate another tool into K8sGPT",
|
||||
Long: `Integrate another tool into K8sGPT. For example:
|
||||
|
||||
k8sgpt integration activate trivy
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ limitations under the License.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -73,7 +74,7 @@ func init() {
|
||||
rootCmd.AddCommand(integration.IntegrationCmd)
|
||||
rootCmd.AddCommand(serve.ServeCmd)
|
||||
rootCmd.AddCommand(cache.CacheCmd)
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.k8sgpt.yaml)")
|
||||
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.")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ package serve
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
@@ -24,10 +25,16 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTemperature float32 = 0.7
|
||||
defaultTopP float32 = 1.0
|
||||
)
|
||||
|
||||
var (
|
||||
port string
|
||||
metricsPort string
|
||||
backend string
|
||||
enableHttp bool
|
||||
)
|
||||
|
||||
var ServeCmd = &cobra.Command{
|
||||
@@ -44,22 +51,59 @@ var ServeCmd = &cobra.Command{
|
||||
}
|
||||
var aiProvider *ai.AIProvider
|
||||
if len(configAI.Providers) == 0 {
|
||||
// we validate and set temperature for our backend
|
||||
temperature := func() float32 {
|
||||
env := os.Getenv("K8SGPT_TEMPERATURE")
|
||||
if env == "" {
|
||||
return defaultTemperature
|
||||
}
|
||||
temperature, err := strconv.ParseFloat(env, 32)
|
||||
if err != nil {
|
||||
color.Red("Unable to convert Temperature value: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if temperature > 1.0 || temperature < 0.0 {
|
||||
color.Red("Error: temperature ranges from 0 to 1.")
|
||||
os.Exit(1)
|
||||
}
|
||||
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)
|
||||
}
|
||||
// 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")
|
||||
// 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,
|
||||
Name: backend,
|
||||
Password: password,
|
||||
Model: model,
|
||||
BaseURL: baseURL,
|
||||
Engine: engine,
|
||||
ProxyEndpoint: proxyEndpoint,
|
||||
Temperature: temperature(),
|
||||
TopP: topP(),
|
||||
}
|
||||
|
||||
configAI.Providers = append(configAI.Providers, *aiProvider)
|
||||
@@ -97,12 +141,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,
|
||||
}
|
||||
@@ -130,4 +180,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.22-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"]
|
||||
|
||||
279
go.mod
279
go.mod
@@ -1,86 +1,157 @@
|
||||
module github.com/k8sgpt-ai/k8sgpt
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/aquasecurity/trivy-operator v0.13.2
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/aquasecurity/trivy-operator v0.17.1
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/kedacore/keda/v2 v2.11.2
|
||||
github.com/magiconair/properties v1.8.7
|
||||
github.com/mittwald/go-helm-client v0.12.1
|
||||
github.com/sashabaranov/go-openai v1.9.3
|
||||
github.com/schollz/progressbar/v3 v3.13.1
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/term v0.9.0
|
||||
helm.sh/helm/v3 v3.12.0
|
||||
k8s.io/api v0.27.2
|
||||
k8s.io/apimachinery v0.27.2
|
||||
k8s.io/client-go v0.27.2
|
||||
k8s.io/kubectl v0.27.2
|
||||
github.com/mittwald/go-helm-client v0.12.5
|
||||
github.com/sashabaranov/go-openai v1.20.4
|
||||
github.com/schollz/progressbar/v3 v3.14.2
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/term v0.18.0
|
||||
helm.sh/helm/v3 v3.13.3
|
||||
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 // indirect
|
||||
|
||||
)
|
||||
|
||||
require github.com/adrg/xdg v0.4.0
|
||||
|
||||
require (
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20230524215339-41d88e13ab7e.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.30.0-20230524215339-41d88e13ab7e.1
|
||||
github.com/aws/aws-sdk-go v1.44.281
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.19.1-20240213144542-6e830f3fdf19.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20240213144542-6e830f3fdf19.2
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.33.0-20240406062209-1cc152efbf5c.1
|
||||
cloud.google.com/go/storage v1.40.0
|
||||
cloud.google.com/go/vertexai v0.7.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1
|
||||
github.com/aws/aws-sdk-go v1.51.21
|
||||
github.com/cohere-ai/cohere-go/v2 v2.7.1
|
||||
github.com/google/generative-ai-go v0.10.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
|
||||
github.com/hupe1980/go-huggingface v0.0.15
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/prometheus/prometheus v0.49.1
|
||||
github.com/pterm/pterm v0.12.79
|
||||
google.golang.org/api v0.170.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
sigs.k8s.io/controller-runtime v0.16.3
|
||||
sigs.k8s.io/gateway-api v1.0.0
|
||||
)
|
||||
|
||||
require github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
require (
|
||||
atomicgo.dev/cursor v0.2.0 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
cloud.google.com/go v0.112.1 // indirect
|
||||
cloud.google.com/go/ai v0.3.0 // indirect
|
||||
cloud.google.com/go/aiplatform v1.60.0 // indirect
|
||||
cloud.google.com/go/compute v1.24.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.7 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.5 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
|
||||
github.com/Microsoft/hcsshim v0.11.4 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect
|
||||
github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 // 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.3 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/prometheus/common/sigv4 v0.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect
|
||||
gopkg.in/evanphx/json-patch.v5 v5.7.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
knative.dev/pkg v0.0.0-20230616134650-eb63a40adfb0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.3 // indirect
|
||||
github.com/aquasecurity/defsec v0.86.0 // indirect
|
||||
github.com/aquasecurity/go-dep-parser v0.0.0-20230413091456-df0396537e15 // 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-20231030050624-4548cca9a5c9 // indirect
|
||||
github.com/aquasecurity/table v1.8.0 // indirect
|
||||
github.com/aquasecurity/tml v0.6.1 // indirect
|
||||
github.com/aquasecurity/trivy v0.40.0 // indirect
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20230411140759-3c2ee2168575 // indirect
|
||||
github.com/aquasecurity/trivy v0.47.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.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/cli v23.0.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker v23.0.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/containerd/containerd v1.7.11 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v24.0.7+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v24.0.7+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.0 // indirect
|
||||
github.com/docker/go-connections v0.4.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.10.2 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // 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.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // 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.3 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // 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.6.9
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/go-containerregistry v0.14.0 // 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/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
@@ -88,22 +159,21 @@ require (
|
||||
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/imdario/mergo v0.3.15 // 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/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.0 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // 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.7 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/masahiro331/go-xfs-filesystem v0.0.0-20221225060805-c02764233454 // indirect
|
||||
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.18 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // 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
|
||||
@@ -111,81 +181,72 @@ require (
|
||||
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/term v0.0.0-20221205130635-1aeaba878587 // 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-rc2.0.20221020182949-4df8887994e8 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // 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.0 // indirect
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rubenv/sql-migrate v1.3.1 // indirect
|
||||
github.com/rubenv/sql-migrate v1.5.2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/samber/lo v1.37.0 // indirect
|
||||
github.com/samber/lo v1.38.1 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spdx/tools-golang v0.5.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spdx/tools-golang v0.5.3 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // 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.1.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-20221020143700-22309ac47eac // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.starlark.net v0.0.0-20231016134836-22325403fcb3 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230124195608-d38c7dcee874 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/oauth2 v0.7.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/text v0.9.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.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/grpc v1.55.0
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/grpc v1.62.1
|
||||
google.golang.org/protobuf v1.33.0 // 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.27.1 // indirect
|
||||
k8s.io/apiserver v0.27.1 // indirect
|
||||
k8s.io/cli-runtime v0.27.2 // indirect
|
||||
k8s.io/component-base v0.27.2 // indirect
|
||||
k8s.io/klog/v2 v2.90.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
|
||||
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 // indirect
|
||||
oras.land/oras-go v1.2.2 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.28.4
|
||||
k8s.io/apiserver v0.28.4 // indirect
|
||||
k8s.io/cli-runtime v0.28.4 // indirect
|
||||
k8s.io/component-base v0.28.4 // indirect
|
||||
k8s.io/klog/v2 v2.110.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/utils v0.0.0-20240310230437-4693a0247e57
|
||||
oras.land/oras-go v1.2.4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.2 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.1 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.15.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.15.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.0 // 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.3
|
||||
|
||||
// v0.3.1-0.20230104082527-d6f58551be3f is taken from github.com/moby/buildkit v0.11.0
|
||||
// spdx logic write on v0.3.0 and incompatible with v0.3.1-0.20230104082527-d6f58551be3f
|
||||
replace github.com/spdx/tools-golang => github.com/spdx/tools-golang v0.3.0
|
||||
replace oras.land/oras-go => oras.land/oras-go v1.2.4
|
||||
|
||||
157
pkg/ai/amazonbedrock.go
Normal file
157
pkg/ai/amazonbedrock.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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
|
||||
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"`
|
||||
}
|
||||
|
||||
// Amazon BedRock support region list US East (N. Virginia),US West (Oregon),Asia Pacific (Singapore),Asia Pacific (Tokyo),Europe (Frankfurt)
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html#bedrock-regions
|
||||
const BEDROCK_DEFAULT_REGION = "us-east-1" // default use us-east-1 region
|
||||
|
||||
const (
|
||||
US_East_1 = "us-east-1"
|
||||
US_West_2 = "us-west-2"
|
||||
AP_Southeast_1 = "ap-southeast-1"
|
||||
AP_Northeast_1 = "ap-northeast-1"
|
||||
EU_Central_1 = "eu-central-1"
|
||||
)
|
||||
|
||||
var BEDROCKER_SUPPORTED_REGION = []string{
|
||||
US_East_1,
|
||||
US_West_2,
|
||||
AP_Southeast_1,
|
||||
AP_Northeast_1,
|
||||
EU_Central_1,
|
||||
}
|
||||
|
||||
const (
|
||||
ModelAnthropicClaudeV2 = "anthropic.claude-v2"
|
||||
ModelAnthropicClaudeV1 = "anthropic.claude-v1"
|
||||
ModelAnthropicClaudeInstantV1 = "anthropic.claude-instant-v1"
|
||||
)
|
||||
|
||||
var BEDROCK_MODELS = []string{
|
||||
ModelAnthropicClaudeV2,
|
||||
ModelAnthropicClaudeV1,
|
||||
ModelAnthropicClaudeInstantV1,
|
||||
}
|
||||
|
||||
// GetModelOrDefault check config model
|
||||
func GetModelOrDefault(model string) string {
|
||||
|
||||
// Check if the provided model is in the list
|
||||
for _, m := range BEDROCK_MODELS {
|
||||
if m == model {
|
||||
return model // Return the provided model
|
||||
}
|
||||
}
|
||||
|
||||
// Return the default model if the provided model is not in the list
|
||||
return BEDROCK_MODELS[0]
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return region // Return the provided model
|
||||
}
|
||||
}
|
||||
|
||||
// Return the default model if the provided model is not in the list
|
||||
return BEDROCK_DEFAULT_REGION
|
||||
}
|
||||
|
||||
// Configure configures the AmazonBedRockClient with the provided configuration.
|
||||
func (a *AmazonBedRockClient) Configure(config IAIConfig) error {
|
||||
|
||||
// Create a new AWS session
|
||||
providerRegion := GetRegionOrDefault(config.GetProviderRegion())
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(providerRegion),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new BedrockRuntime client
|
||||
a.client = bedrockruntime.New(sess)
|
||||
a.model = GetModelOrDefault(config.GetModel())
|
||||
a.temperature = config.GetTemperature()
|
||||
|
||||
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) (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,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Build the parameters for the model invocation
|
||||
params := &bedrockruntime.InvokeModelInput{
|
||||
Body: body,
|
||||
ModelId: aws.String(a.model),
|
||||
ContentType: aws.String("application/json"),
|
||||
Accept: aws.String("application/json"),
|
||||
}
|
||||
// Invoke the model
|
||||
resp, err := a.client.InvokeModelWithContext(ctx, params)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetName returns the name of the AmazonBedRockClient.
|
||||
func (a *AmazonBedRockClient) GetName() string {
|
||||
return amazonbedrockAIClientName
|
||||
}
|
||||
137
pkg/ai/amazonsagemaker.go
Normal file
137
pkg/ai/amazonsagemaker.go
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
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"
|
||||
"encoding/json"
|
||||
"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
|
||||
model string
|
||||
temperature float32
|
||||
endpoint string
|
||||
topP float32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
type Generations []struct {
|
||||
Generation struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
} `json:"generation"`
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Inputs [][]Message `json:"inputs"`
|
||||
Parameters Parameters `json:"parameters"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Parameters struct {
|
||||
MaxNewTokens int `json:"max_new_tokens"`
|
||||
TopP float64 `json:"top_p"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
func (c *SageMakerAIClient) Configure(config IAIConfig) error {
|
||||
|
||||
// Create a new AWS session
|
||||
sess := session.Must(session.NewSessionWithOptions(session.Options{
|
||||
Config: aws.Config{Region: aws.String(config.GetProviderRegion())},
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
}))
|
||||
|
||||
// Create a new SageMaker runtime client
|
||||
c.client = sagemakerruntime.New(sess)
|
||||
c.model = config.GetModel()
|
||||
c.endpoint = config.GetEndpointName()
|
||||
c.temperature = config.GetTemperature()
|
||||
c.maxTokens = config.GetMaxTokens()
|
||||
c.topP = config.GetTopP()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *SageMakerAIClient) GetCompletion(_ context.Context, prompt string) (string, error) {
|
||||
// Create a completion request
|
||||
request := Request{
|
||||
Inputs: [][]Message{
|
||||
{
|
||||
{Role: "system", Content: "DEFAULT_PROMPT"},
|
||||
{Role: "user", Content: prompt},
|
||||
},
|
||||
},
|
||||
|
||||
Parameters: Parameters{
|
||||
MaxNewTokens: int(c.maxTokens),
|
||||
TopP: float64(c.topP),
|
||||
Temperature: float64(c.temperature),
|
||||
},
|
||||
}
|
||||
|
||||
// Convert request to []byte
|
||||
bytesData, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create an input object
|
||||
input := &sagemakerruntime.InvokeEndpointInput{
|
||||
Body: bytesData,
|
||||
EndpointName: aws.String(c.endpoint),
|
||||
ContentType: aws.String("application/json"), // Set the content type as per your model's requirements
|
||||
Accept: aws.String("application/json"), // Set the accept type as per your model's requirements
|
||||
CustomAttributes: aws.String("accept_eula=true"),
|
||||
}
|
||||
|
||||
// Call the InvokeEndpoint function
|
||||
result, err := c.client.InvokeEndpoint(input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// // Define a slice of Generations
|
||||
var generations Generations
|
||||
|
||||
err = json.Unmarshal([]byte(string(result.Body)), &generations)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Check for length of generations
|
||||
if len(generations) != 1 {
|
||||
return "", fmt.Errorf("Expected exactly one generation, but got %d", len(generations))
|
||||
}
|
||||
|
||||
// Access the content
|
||||
content := generations[0].Generation.Content
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func (c *SageMakerAIClient) GetName() string {
|
||||
return amazonsagemakerAIClientName
|
||||
}
|
||||
@@ -2,50 +2,73 @@ 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 {
|
||||
client *openai.Client
|
||||
language string
|
||||
model string
|
||||
nopCloser
|
||||
|
||||
client *openai.Client
|
||||
model string
|
||||
temperature float32
|
||||
}
|
||||
|
||||
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()
|
||||
defaultConfig := openai.DefaultAzureConfig(token, baseURL, engine)
|
||||
proxyEndpoint := config.GetProxyEndpoint()
|
||||
defaultConfig := openai.DefaultAzureConfig(token, baseURL)
|
||||
|
||||
defaultConfig.AzureModelMapperFunc = func(model string) string {
|
||||
// If you use a deployment name different from the model name, you can customize the AzureModelMapperFunc function
|
||||
azureModelMapping := map[string]string{
|
||||
model: engine,
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
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: "user",
|
||||
Content: fmt.Sprintf(default_prompt, c.language, prompt),
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
Temperature: c.temperature,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -53,42 +76,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
|
||||
}
|
||||
|
||||
80
pkg/ai/cohere.go
Normal file
80
pkg/ai/cohere.go
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
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
|
||||
model string
|
||||
temperature float32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
func (c *CohereClient) Configure(config IAIConfig) error {
|
||||
token := config.GetPassword()
|
||||
|
||||
opts := []option.RequestOption{
|
||||
cohere.WithToken(token),
|
||||
}
|
||||
|
||||
baseURL := config.GetBaseURL()
|
||||
if baseURL != "" {
|
||||
opts = append(opts, cohere.WithBaseURL(baseURL))
|
||||
}
|
||||
|
||||
client := cohere.NewClient(opts...)
|
||||
if client == nil {
|
||||
return errors.New("error creating Cohere client")
|
||||
}
|
||||
|
||||
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 string) (string, error) {
|
||||
// Create a completion request
|
||||
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 response.Text, nil
|
||||
}
|
||||
|
||||
func (c *CohereClient) GetName() string {
|
||||
return cohereAIClientName
|
||||
}
|
||||
119
pkg/ai/googlegenai.go
Normal file
119
pkg/ai/googlegenai.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
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
|
||||
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.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.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)
|
||||
}
|
||||
}
|
||||
178
pkg/ai/googlevertexai.go
Normal file
178
pkg/ai/googlevertexai.go
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
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
|
||||
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.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.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)
|
||||
}
|
||||
}
|
||||
59
pkg/ai/huggingface.go
Normal file
59
pkg/ai/huggingface.go
Normal file
@@ -0,0 +1,59 @@
|
||||
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
|
||||
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.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)),
|
||||
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 }
|
||||
100
pkg/ai/iai.go
100
pkg/ai/iai.go
@@ -15,8 +15,6 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -25,27 +23,57 @@ var (
|
||||
&AzureAIClient{},
|
||||
&LocalAIClient{},
|
||||
&NoOpAIClient{},
|
||||
&CohereClient{},
|
||||
&AmazonBedRockClient{},
|
||||
&SageMakerAIClient{},
|
||||
&GoogleGenAIClient{},
|
||||
&HuggingfaceClient{},
|
||||
&GoogleVertexAIClient{},
|
||||
}
|
||||
Backends = []string{
|
||||
"openai",
|
||||
"localai",
|
||||
"azureopenai",
|
||||
"noopai",
|
||||
openAIClientName,
|
||||
localAIClientName,
|
||||
azureAIClientName,
|
||||
cohereAIClientName,
|
||||
amazonbedrockAIClientName,
|
||||
amazonsagemakerAIClientName,
|
||||
googleAIClientName,
|
||||
noopAIClientName,
|
||||
huggingfaceAIClientName,
|
||||
googleVertexAIClientName,
|
||||
}
|
||||
)
|
||||
|
||||
// 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
|
||||
GetMaxTokens() int
|
||||
GetProviderId() string
|
||||
}
|
||||
|
||||
func NewClient(provider string) IAI {
|
||||
@@ -64,17 +92,41 @@ 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"`
|
||||
Engine string `mapstructure:"engine" yaml:"engine,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"`
|
||||
TopP float32 `mapstructure:"topp" yaml:"topp,omitempty"`
|
||||
MaxTokens int `mapstructure:"maxtokens" yaml:"maxtokens,omitempty"`
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetBaseURL() string {
|
||||
return p.BaseURL
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetProxyEndpoint() string {
|
||||
return p.ProxyEndpoint
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetEndpointName() string {
|
||||
return p.EndpointName
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetTopP() float32 {
|
||||
return p.TopP
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetMaxTokens() int {
|
||||
return p.MaxTokens
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetPassword() string {
|
||||
return p.Password
|
||||
}
|
||||
@@ -86,7 +138,25 @@ func (p *AIProvider) GetModel() string {
|
||||
func (p *AIProvider) GetEngine() string {
|
||||
return p.Engine
|
||||
}
|
||||
func (p *AIProvider) GetTemperature() float32 {
|
||||
return p.Temperature
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetProviderRegion() string {
|
||||
return p.ProviderRegion
|
||||
}
|
||||
|
||||
func (p *AIProvider) GetProviderId() string {
|
||||
return p.ProviderId
|
||||
}
|
||||
|
||||
var passwordlessProviders = []string{"localai", "amazonsagemaker", "amazonbedrock", "googlevertexai"}
|
||||
|
||||
func NeedPassword(backend string) bool {
|
||||
return backend != "localai"
|
||||
for _, b := range passwordlessProviders {
|
||||
if b == backend {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
100
pkg/ai/openai.go
100
pkg/ai/openai.go
@@ -15,57 +15,81 @@ 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 {
|
||||
client *openai.Client
|
||||
language string
|
||||
model string
|
||||
nopCloser
|
||||
|
||||
client *openai.Client
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
}
|
||||
|
||||
func (c *OpenAIClient) Configure(config IAIConfig, language string) error {
|
||||
const (
|
||||
// OpenAI completion parameters
|
||||
maxToken = 2048
|
||||
presencePenalty = 0.0
|
||||
frequencyPenalty = 0.0
|
||||
)
|
||||
|
||||
func (c *OpenAIClient) Configure(config IAIConfig) error {
|
||||
token := config.GetPassword()
|
||||
defaultConfig := openai.DefaultConfig(token)
|
||||
proxyEndpoint := config.GetProxyEndpoint()
|
||||
|
||||
baseURL := config.GetBaseURL()
|
||||
if baseURL != "" {
|
||||
defaultConfig.BaseURL = baseURL
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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: c.topP,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -73,42 +97,6 @@ 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)
|
||||
|
||||
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 *OpenAIClient) GetName() string {
|
||||
return "openai"
|
||||
func (c *OpenAIClient) GetName() string {
|
||||
return openAIClientName
|
||||
}
|
||||
|
||||
@@ -6,10 +6,54 @@ const (
|
||||
Error: {Explain error here}
|
||||
Solution: {Step by step solution here}
|
||||
`
|
||||
trivy_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_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}
|
||||
`
|
||||
)
|
||||
|
||||
var PromptMap = map[string]string{
|
||||
"default": default_prompt,
|
||||
"VulnerabilityReport": trivy_prompt, // for Trivy intergration, the key should match `Result.Kind` in pkg/common/types.go
|
||||
"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,
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ package analysis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -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,6 +39,7 @@ type Analysis struct {
|
||||
Context context.Context
|
||||
Filters []string
|
||||
Client *kubernetes.Client
|
||||
Language string
|
||||
AIClient ai.IAI
|
||||
Results []common.Result
|
||||
Errors []string
|
||||
@@ -49,8 +51,10 @@ type Analysis struct {
|
||||
WithDoc bool
|
||||
}
|
||||
|
||||
type AnalysisStatus string
|
||||
type AnalysisErrors []string
|
||||
type (
|
||||
AnalysisStatus string
|
||||
AnalysisErrors []string
|
||||
)
|
||||
|
||||
const (
|
||||
StateOK AnalysisStatus = "OK"
|
||||
@@ -65,25 +69,70 @@ 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,
|
||||
noCache bool,
|
||||
explain bool,
|
||||
maxConcurrency int,
|
||||
withDoc bool,
|
||||
interactiveMode 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,
|
||||
Cache: cache,
|
||||
Explain: explain,
|
||||
MaxConcurrency: maxConcurrency,
|
||||
WithDoc: withDoc,
|
||||
}
|
||||
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,45 +142,37 @@ 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)
|
||||
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
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
// load remote cache if it is configured
|
||||
remoteCacheEnabled, err := cache.RemoteCacheEnabled()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, cAnalyzer := range customAnalyzers {
|
||||
|
||||
return &Analysis{
|
||||
Context: ctx,
|
||||
Filters: filters,
|
||||
Client: client,
|
||||
AIClient: aiClient,
|
||||
Namespace: namespace,
|
||||
Cache: cache.New(noCache, remoteCacheEnabled),
|
||||
Explain: explain,
|
||||
MaxConcurrency: maxConcurrency,
|
||||
AnalysisAIProvider: backend,
|
||||
WithDoc: withDoc,
|
||||
}, nil
|
||||
canClient, err := custom.NewClient(cAnalyzer.Connection)
|
||||
if err != nil {
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("Client creation error for %s analyzer", cAnalyzer.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := canClient.Run()
|
||||
if err != nil {
|
||||
a.Results = append(a.Results, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Analysis) RunAnalysis() {
|
||||
@@ -261,14 +302,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?
|
||||
@@ -276,23 +317,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)
|
||||
}
|
||||
@@ -300,3 +340,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()
|
||||
}
|
||||
|
||||
@@ -58,8 +58,12 @@ func (a *Analysis) jsonOutput() ([]byte, error) {
|
||||
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 +78,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 != "" {
|
||||
|
||||
@@ -32,21 +32,27 @@ var (
|
||||
)
|
||||
|
||||
var coreAnalyzerMap = map[string]common.IAnalyzer{
|
||||
"Pod": PodAnalyzer{},
|
||||
"Deployment": DeploymentAnalyzer{},
|
||||
"ReplicaSet": ReplicaSetAnalyzer{},
|
||||
"PersistentVolumeClaim": PvcAnalyzer{},
|
||||
"Service": ServiceAnalyzer{},
|
||||
"Ingress": IngressAnalyzer{},
|
||||
"StatefulSet": StatefulSetAnalyzer{},
|
||||
"CronJob": CronJobAnalyzer{},
|
||||
"Node": NodeAnalyzer{},
|
||||
"Pod": PodAnalyzer{},
|
||||
"Deployment": DeploymentAnalyzer{},
|
||||
"ReplicaSet": ReplicaSetAnalyzer{},
|
||||
"PersistentVolumeClaim": PvcAnalyzer{},
|
||||
"Service": ServiceAnalyzer{},
|
||||
"Ingress": IngressAnalyzer{},
|
||||
"StatefulSet": StatefulSetAnalyzer{},
|
||||
"CronJob": CronJobAnalyzer{},
|
||||
"Node": NodeAnalyzer{},
|
||||
"ValidatingWebhookConfiguration": ValidatingWebhookAnalyzer{},
|
||||
"MutatingWebhookConfiguration": MutatingWebhookAnalyzer{},
|
||||
}
|
||||
|
||||
var additionalAnalyzerMap = map[string]common.IAnalyzer{
|
||||
"HorizontalPodAutoScaler": HpaAnalyzer{},
|
||||
"PodDisruptionBudget": PdbAnalyzer{},
|
||||
"NetworkPolicy": NetworkPolicyAnalyzer{},
|
||||
"Log": LogAnalyzer{},
|
||||
"GatewayClass": GatewayClassAnalyzer{},
|
||||
"Gateway": GatewayAnalyzer{},
|
||||
"HTTPRoute": HTTPRouteAnalyzer{},
|
||||
}
|
||||
|
||||
func ListFilters() ([]string, []string, []string) {
|
||||
@@ -71,7 +77,7 @@ func ListFilters() ([]string, []string, []string) {
|
||||
fmt.Println(color.RedString(err.Error()))
|
||||
os.Exit(1)
|
||||
}
|
||||
integrationAnalyzers = append(integrationAnalyzers, in.GetAnalyzerName())
|
||||
integrationAnalyzers = append(integrationAnalyzers, in.GetAnalyzerName()...)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,144 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
111
pkg/analyzer/gateway.go
Normal file
111
pkg/analyzer/gateway.go
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
type GatewayAnalyzer struct{}
|
||||
|
||||
// Gateway analyser will analyse all different Kinds and search for missing object dependencies
|
||||
func (GatewayAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "Gateway"
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
gtwList := >wapi.GatewayList{}
|
||||
gc := >wapi.GatewayClass{}
|
||||
client := a.Client.CtrlClient
|
||||
err := gtwapi.AddToScheme(client.Scheme())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.List(a.Context, gtwList, &ctrl.ListOptions{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
// Find all unhealthy gateway Classes
|
||||
|
||||
for _, gtw := range gtwList.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
gtwName := gtw.GetName()
|
||||
gtwNamespace := gtw.GetNamespace()
|
||||
// Check if gatewayclass exists
|
||||
err := client.Get(a.Context, ctrl.ObjectKey{Namespace: gtwNamespace, Name: string(gtw.Spec.GatewayClassName)}, gc, &ctrl.GetOptions{})
|
||||
if errors.IsNotFound(err) {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf(
|
||||
"Gateway uses the GatewayClass %s which does not exist.",
|
||||
gtw.Spec.GatewayClassName,
|
||||
),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: string(gtw.Spec.GatewayClassName),
|
||||
Masked: util.MaskString(string(gtw.Spec.GatewayClassName)),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Check only the current conditions
|
||||
// TODO: maybe check other statuses Listeners, addresses?
|
||||
if gtw.Status.Conditions[0].Status != metav1.ConditionTrue {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Gateway '%s/%s' is not accepted. Message: '%s'.",
|
||||
gtwNamespace,
|
||||
gtwName,
|
||||
gtw.Status.Conditions[0].Message,
|
||||
),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: gtwNamespace,
|
||||
Masked: util.MaskString(gtwNamespace),
|
||||
},
|
||||
{
|
||||
Unmasked: gtwName,
|
||||
Masked: util.MaskString(gtwName),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", gtwNamespace, gtwName)] = common.PreAnalysis{
|
||||
Gateway: gtw,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, gtwName, gtwNamespace).Set(float64(len(failures)))
|
||||
}
|
||||
}
|
||||
for key, value := range preAnalysis {
|
||||
var currentAnalysis = common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
return a.Results, nil
|
||||
}
|
||||
180
pkg/analyzer/gateway_test.go
Normal file
180
pkg/analyzer/gateway_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/magiconair/properties/assert"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
func BuildGatewayClass(name string) gtwapi.GatewayClass {
|
||||
GatewayClass := gtwapi.GatewayClass{}
|
||||
GatewayClass.Name = name
|
||||
// Namespace is not needed outside of this test, GatewayClass is cluster-scoped
|
||||
GatewayClass.Namespace = "default"
|
||||
GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller"
|
||||
|
||||
return GatewayClass
|
||||
}
|
||||
|
||||
func BuildGateway(className gtwapi.ObjectName, status metav1.ConditionStatus) gtwapi.Gateway {
|
||||
Gateway := gtwapi.Gateway{}
|
||||
Gateway.Name = "foobar"
|
||||
Gateway.Namespace = "default"
|
||||
Gateway.Spec.GatewayClassName = className
|
||||
Gateway.Spec.Listeners = []gtwapi.Listener{
|
||||
{
|
||||
Name: "proxy",
|
||||
Port: 80,
|
||||
Protocol: gtwapi.HTTPProtocolType,
|
||||
},
|
||||
}
|
||||
Condition := metav1.Condition{
|
||||
Type: "Accepted",
|
||||
Status: status,
|
||||
Message: "An expected message",
|
||||
Reason: "Test",
|
||||
}
|
||||
Gateway.Status.Conditions = []metav1.Condition{Condition}
|
||||
|
||||
return Gateway
|
||||
}
|
||||
|
||||
func TestGatewayAnalyzer(t *testing.T) {
|
||||
ClassName := gtwapi.ObjectName("exists")
|
||||
AcceptedStatus := metav1.ConditionTrue
|
||||
GatewayClass := BuildGatewayClass(string(ClassName))
|
||||
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus)
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
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,
|
||||
&GatewayClass,
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
|
||||
|
||||
analyzerInstance := GatewayAnalyzer{}
|
||||
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), 0)
|
||||
|
||||
}
|
||||
|
||||
func TestMissingClassGatewayAnalyzer(t *testing.T) {
|
||||
ClassName := gtwapi.ObjectName("non-existed")
|
||||
AcceptedStatus := metav1.ConditionTrue
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus)
|
||||
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
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{}
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
func TestStatusGatewayAnalyzer(t *testing.T) {
|
||||
ClassName := gtwapi.ObjectName("exists")
|
||||
AcceptedStatus := metav1.ConditionUnknown
|
||||
GatewayClass := BuildGatewayClass(string(ClassName))
|
||||
|
||||
Gateway := BuildGateway(ClassName, AcceptedStatus)
|
||||
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
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,
|
||||
&GatewayClass,
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
|
||||
|
||||
analyzerInstance := GatewayAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var errorFound bool
|
||||
want := "Gateway 'default/foobar' is not accepted. Message: 'An expected message'."
|
||||
for _, analysis := range analysisResults {
|
||||
for _, got := range analysis.Error {
|
||||
if want == got.Text {
|
||||
errorFound = true
|
||||
}
|
||||
}
|
||||
if errorFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !errorFound {
|
||||
t.Errorf("Expected message, <%v> , not found in Gateway's analysis results", want)
|
||||
}
|
||||
}
|
||||
87
pkg/analyzer/gatewayclass.go
Normal file
87
pkg/analyzer/gatewayclass.go
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
type GatewayClassAnalyzer struct{}
|
||||
|
||||
// Gateway analyser will analyse all different Kinds and search for missing object dependencies
|
||||
func (GatewayClassAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "GatewayClass"
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
gcList := >wapi.GatewayClassList{}
|
||||
client := a.Client.CtrlClient
|
||||
err := gtwapi.AddToScheme(client.Scheme())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.List(a.Context, gcList, &ctrl.ListOptions{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
// Find all unhealthy gateway Classes
|
||||
|
||||
for _, gc := range gcList.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
gcName := gc.GetName()
|
||||
// Check only the current condition
|
||||
if gc.Status.Conditions[0].Status != metav1.ConditionTrue {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf(
|
||||
"GatewayClass '%s' with a controller name '%s' is not accepted. Message: '%s'.",
|
||||
gcName,
|
||||
gc.Spec.ControllerName,
|
||||
gc.Status.Conditions[0].Message,
|
||||
),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: gcName,
|
||||
Masked: util.MaskString(gcName),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[gcName] = common.PreAnalysis{
|
||||
GatewayClass: gc,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, gcName, "").Set(float64(len(failures)))
|
||||
}
|
||||
}
|
||||
for key, value := range preAnalysis {
|
||||
var currentAnalysis = common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
return a.Results, nil
|
||||
}
|
||||
57
pkg/analyzer/gatewayclass_test.go
Normal file
57
pkg/analyzer/gatewayclass_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
// Testing with the fake dynamic client if GatewayClasses have an accepted status
|
||||
func TestGatewayClassAnalyzer(t *testing.T) {
|
||||
GatewayClass := >wapi.GatewayClass{}
|
||||
GatewayClass.Name = "foobar"
|
||||
GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller"
|
||||
// Initialize Conditions slice before setting properties
|
||||
BadCondition := metav1.Condition{
|
||||
Type: "Accepted",
|
||||
Status: "Uknown",
|
||||
Message: "Waiting for controller",
|
||||
Reason: "Pending",
|
||||
}
|
||||
GatewayClass.Status.Conditions = []metav1.Condition{BadCondition}
|
||||
// Create a GatewayClassAnalyzer instance with the fake client
|
||||
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).Build()
|
||||
|
||||
analyzerInstance := GatewayClassAnalyzer{}
|
||||
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)
|
||||
|
||||
}
|
||||
@@ -140,8 +140,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)
|
||||
}
|
||||
|
||||
|
||||
230
pkg/analyzer/httproute.go
Normal file
230
pkg/analyzer/httproute.go
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
type HTTPRouteAnalyzer struct{}
|
||||
|
||||
// Gateway analyser will analyse all different Kinds and search for missing object dependencies
|
||||
func (HTTPRouteAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "HTTPRoute"
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
routeList := >wapi.HTTPRouteList{}
|
||||
gtw := >wapi.Gateway{}
|
||||
service := &corev1.Service{}
|
||||
client := a.Client.CtrlClient
|
||||
err := gtwapi.AddToScheme(client.Scheme())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.List(a.Context, routeList, &ctrl.ListOptions{}); 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
|
||||
|
||||
// Check if Gateways exists in the same or designated namespace
|
||||
// TODO: when meshes and ClusterIp options are adopted we can add more checks
|
||||
// e.g Service Port matching
|
||||
for _, gtwref := range route.Spec.ParentRefs {
|
||||
namespace := route.Namespace
|
||||
if gtwref.Namespace != nil {
|
||||
namespace = string(*gtwref.Namespace)
|
||||
}
|
||||
err := client.Get(a.Context, ctrl.ObjectKey{Namespace: namespace, Name: string(gtwref.Name)}, gtw, &ctrl.GetOptions{})
|
||||
if errors.IsNotFound(err) {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf(
|
||||
"HTTPRoute uses the Gateway '%s/%s' which does not exist in the same namespace.",
|
||||
namespace,
|
||||
gtwref.Name,
|
||||
),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: gtw.Namespace,
|
||||
Masked: util.MaskString(gtw.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: gtw.Name,
|
||||
Masked: util.MaskString(gtw.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Check if the aforementioned Gateway allows the HTTPRoutes from the route's namespace
|
||||
for _, listener := range gtw.Spec.Listeners {
|
||||
if listener.AllowedRoutes.Namespaces != nil {
|
||||
switch allow := listener.AllowedRoutes.Namespaces.From; {
|
||||
case *allow == gtwapi.NamespacesFromSame:
|
||||
// check if Gateway is in the same namespace
|
||||
if route.Namespace != gtw.Namespace {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("HTTPRoute '%s/%s' is deployed in a different namespace from Gateway '%s/%s' which only allows HTTPRoutes from its namespace.",
|
||||
route.Namespace,
|
||||
route.Name,
|
||||
gtw.Namespace,
|
||||
gtw.Name,
|
||||
),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: route.Namespace,
|
||||
Masked: util.MaskString(route.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: route.Name,
|
||||
Masked: util.MaskString(route.Name),
|
||||
},
|
||||
{
|
||||
Unmasked: gtw.Namespace,
|
||||
Masked: util.MaskString(gtw.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: gtw.Name,
|
||||
Masked: util.MaskString(gtw.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
case *allow == gtwapi.NamespacesFromSelector:
|
||||
// check if our route include the same selector Label
|
||||
if !util.LabelsIncludeAny(listener.AllowedRoutes.Namespaces.Selector.MatchLabels, route.Labels) {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf(
|
||||
"HTTPRoute '%s/%s' can't be attached on Gateway '%s/%s', selector labels do not match HTTProute's labels.",
|
||||
route.Namespace,
|
||||
route.Name,
|
||||
gtw.Namespace,
|
||||
gtw.Name,
|
||||
),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: route.Namespace,
|
||||
Masked: util.MaskString(route.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: route.Name,
|
||||
Masked: util.MaskString(route.Name),
|
||||
},
|
||||
{
|
||||
Unmasked: gtw.Namespace,
|
||||
Masked: util.MaskString(gtw.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: gtw.Name,
|
||||
Masked: util.MaskString(gtw.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// Check if the Backends are valid services and ports are matching with services Ports
|
||||
for _, rule := range route.Spec.Rules {
|
||||
for _, backend := range rule.BackendRefs {
|
||||
err := client.Get(a.Context, ctrl.ObjectKey{Namespace: route.Namespace, Name: string(backend.Name)}, service, &ctrl.GetOptions{})
|
||||
if errors.IsNotFound(err) {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf(
|
||||
"HTTPRoute uses the Service '%s/%s' which does not exist.",
|
||||
route.Namespace,
|
||||
backend.Name,
|
||||
),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: service.Namespace,
|
||||
Masked: util.MaskString(service.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: service.Name,
|
||||
Masked: util.MaskString(service.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
portMatch := false
|
||||
for _, svcPort := range service.Spec.Ports {
|
||||
if int32(*backend.Port) == svcPort.Port {
|
||||
portMatch = true
|
||||
}
|
||||
}
|
||||
if !portMatch {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf(
|
||||
"HTTPRoute's backend service '%s' is using port '%d' but the corresponding K8s service '%s/%s' isn't configured with the same port.",
|
||||
backend.Name,
|
||||
int32(*backend.Port),
|
||||
service.Namespace,
|
||||
service.Name,
|
||||
),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: string(backend.Name),
|
||||
Masked: util.MaskString(string(backend.Name)),
|
||||
},
|
||||
{
|
||||
Unmasked: service.Name,
|
||||
Masked: util.MaskString(service.Name),
|
||||
},
|
||||
{
|
||||
Unmasked: service.Namespace,
|
||||
Masked: service.Namespace,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", route.Namespace, route.Name)] = common.PreAnalysis{
|
||||
HTTPRoute: route,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, route.Name, route.Namespace).Set(float64(len(failures)))
|
||||
}
|
||||
}
|
||||
for key, value := range preAnalysis {
|
||||
var currentAnalysis = common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
return a.Results, nil
|
||||
|
||||
}
|
||||
404
pkg/analyzer/httproute_test.go
Normal file
404
pkg/analyzer/httproute_test.go
Normal file
@@ -0,0 +1,404 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
func BuildRouteGateway(namespace, name, fromNamespaceref string) gtwapi.Gateway {
|
||||
routeNamespace := >wapi.RouteNamespaces{}
|
||||
switch fromNamespaceref {
|
||||
case "Same":
|
||||
fromSame := gtwapi.NamespacesFromSame
|
||||
routeNamespace.From = &fromSame
|
||||
case "Selector":
|
||||
fromSelector := gtwapi.NamespacesFromSelector
|
||||
routeNamespace.From = &fromSelector
|
||||
routeNamespace.Selector = &metav1.LabelSelector{}
|
||||
routeNamespace.Selector.MatchLabels = map[string]string{"foo": "bar"}
|
||||
|
||||
default:
|
||||
fromAll := gtwapi.NamespacesFromAll
|
||||
routeNamespace.From = &fromAll
|
||||
}
|
||||
Gateway := gtwapi.Gateway{}
|
||||
Gateway.Name = name
|
||||
Gateway.Namespace = namespace
|
||||
Gateway.Spec.GatewayClassName = "fooclassName"
|
||||
Gateway.Spec.Listeners = []gtwapi.Listener{
|
||||
{
|
||||
Name: "proxy",
|
||||
Port: 80,
|
||||
Protocol: gtwapi.HTTPProtocolType,
|
||||
AllowedRoutes: >wapi.AllowedRoutes{
|
||||
Namespaces: routeNamespace,
|
||||
},
|
||||
},
|
||||
}
|
||||
Condition := metav1.Condition{
|
||||
Type: "Accepted",
|
||||
Status: "True",
|
||||
Message: "An expected message",
|
||||
Reason: "Test",
|
||||
}
|
||||
Gateway.Status.Conditions = []metav1.Condition{Condition}
|
||||
|
||||
return Gateway
|
||||
}
|
||||
|
||||
func BuildHTTPRoute(backendName, gtwName gtwapi.ObjectName, gtwNamespace gtwapi.Namespace, svcPort *gtwapi.PortNumber, namespace string) gtwapi.HTTPRoute {
|
||||
HTTPRoute := gtwapi.HTTPRoute{}
|
||||
HTTPRoute.Name = "foohttproute"
|
||||
HTTPRoute.Namespace = namespace
|
||||
HTTPRoute.Spec.ParentRefs = []gtwapi.ParentReference{
|
||||
{
|
||||
Name: gtwName,
|
||||
Namespace: >wNamespace,
|
||||
},
|
||||
}
|
||||
HTTPRoute.Spec.Rules = []gtwapi.HTTPRouteRule{
|
||||
{
|
||||
BackendRefs: []gtwapi.HTTPBackendRef{
|
||||
{
|
||||
BackendRef: gtwapi.BackendRef{
|
||||
BackendObjectReference: gtwapi.BackendObjectReference{
|
||||
Name: backendName,
|
||||
Port: svcPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return HTTPRoute
|
||||
}
|
||||
|
||||
/*
|
||||
Testing different cases
|
||||
|
||||
1. Gateway doesn't exist or at least doesn't exist in the same namespace
|
||||
2. Gateway exists in different namespace, is configured in httproute's spec
|
||||
and Gateway's configuration is allowing only from its same namespace
|
||||
3. Gateway exists in the same namespace but has selectors different from route's labels
|
||||
4. BackendRef is pointing to a non existent Service
|
||||
5. BackendRef's port and Service Port are different
|
||||
*/
|
||||
func TestGWMissiningHTTRouteAnalyzer(t *testing.T) {
|
||||
backendName := gtwapi.ObjectName("foobackend")
|
||||
gtwName := gtwapi.ObjectName("non-existent")
|
||||
gtwNamespace := gtwapi.Namespace("non-existent")
|
||||
svcPort := gtwapi.PortNumber(1027)
|
||||
httpRouteNamespace := "default"
|
||||
|
||||
HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace)
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
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{
|
||||
&HTTPRoute,
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
|
||||
|
||||
analyzerInstance := HTTPRouteAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var errorFound bool
|
||||
want := "HTTPRoute uses the Gateway 'non-existent/non-existent' which does not exist in the same namespace."
|
||||
for _, analysis := range analysisResults {
|
||||
for _, got := range analysis.Error {
|
||||
if want == got.Text {
|
||||
errorFound = true
|
||||
}
|
||||
}
|
||||
if errorFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !errorFound {
|
||||
t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGWConfigSameHTTRouteAnalyzer(t *testing.T) {
|
||||
backendName := gtwapi.ObjectName("foobackend")
|
||||
gtwName := gtwapi.ObjectName("gatewayname")
|
||||
gtwNamespace := gtwapi.Namespace("differentnamespace")
|
||||
svcPort := gtwapi.PortNumber(1027)
|
||||
httpRouteNamespace := "default"
|
||||
|
||||
HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace)
|
||||
|
||||
Gateway := BuildRouteGateway("differentnamespace", "gatewayname", "Same")
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
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{
|
||||
&HTTPRoute,
|
||||
&Gateway,
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
|
||||
|
||||
analyzerInstance := HTTPRouteAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var errorFound bool
|
||||
want := "HTTPRoute 'default/foohttproute' is deployed in a different namespace from Gateway 'differentnamespace/gatewayname' which only allows HTTPRoutes from its namespace."
|
||||
for _, analysis := range analysisResults {
|
||||
for _, got := range analysis.Error {
|
||||
if want == got.Text {
|
||||
errorFound = true
|
||||
}
|
||||
}
|
||||
if errorFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !errorFound {
|
||||
t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want)
|
||||
}
|
||||
}
|
||||
func TestGWConfigSelectorHTTRouteAnalyzer(t *testing.T) {
|
||||
backendName := gtwapi.ObjectName("foobackend")
|
||||
gtwName := gtwapi.ObjectName("gatewayname")
|
||||
gtwNamespace := gtwapi.Namespace("default")
|
||||
svcPort := gtwapi.PortNumber(1027)
|
||||
httpRouteNamespace := "default"
|
||||
|
||||
HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace)
|
||||
|
||||
Gateway := BuildRouteGateway("default", "gatewayname", "Selector")
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
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{
|
||||
&HTTPRoute,
|
||||
&Gateway,
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
|
||||
|
||||
analyzerInstance := HTTPRouteAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var errorFound bool
|
||||
want := "HTTPRoute 'default/foohttproute' can't be attached on Gateway 'default/gatewayname', selector labels do not match HTTProute's labels."
|
||||
for _, analysis := range analysisResults {
|
||||
for _, got := range analysis.Error {
|
||||
if want == got.Text {
|
||||
errorFound = true
|
||||
}
|
||||
}
|
||||
if errorFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !errorFound {
|
||||
t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSvcMissingHTTRouteAnalyzer(t *testing.T) {
|
||||
backendName := gtwapi.ObjectName("foobackend")
|
||||
gtwName := gtwapi.ObjectName("gatewayname")
|
||||
gtwNamespace := gtwapi.Namespace("default")
|
||||
svcPort := gtwapi.PortNumber(1027)
|
||||
httpRouteNamespace := "default"
|
||||
|
||||
HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace)
|
||||
|
||||
Gateway := BuildRouteGateway("default", "gatewayname", "Same")
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
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{
|
||||
&HTTPRoute,
|
||||
&Gateway,
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
|
||||
|
||||
analyzerInstance := HTTPRouteAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var errorFound bool
|
||||
want := "HTTPRoute uses the Service 'default/foobackend' which does not exist."
|
||||
for _, analysis := range analysisResults {
|
||||
for _, got := range analysis.Error {
|
||||
if want == got.Text {
|
||||
errorFound = true
|
||||
}
|
||||
}
|
||||
if errorFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !errorFound {
|
||||
t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want)
|
||||
}
|
||||
}
|
||||
func TestSvcDifferentPortHTTRouteAnalyzer(t *testing.T) {
|
||||
//Add a Service Object
|
||||
Service := corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foobackend",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
Ports: []corev1.ServicePort{
|
||||
{
|
||||
Name: "http",
|
||||
Protocol: "TCP",
|
||||
Port: 80,
|
||||
TargetPort: intstr.FromInt(8080),
|
||||
},
|
||||
},
|
||||
Type: corev1.ServiceTypeClusterIP,
|
||||
},
|
||||
}
|
||||
backendName := gtwapi.ObjectName("foobackend")
|
||||
gtwName := gtwapi.ObjectName("gatewayname")
|
||||
gtwNamespace := gtwapi.Namespace("default")
|
||||
// different port
|
||||
svcPort := gtwapi.PortNumber(1027)
|
||||
httpRouteNamespace := "default"
|
||||
|
||||
HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace)
|
||||
|
||||
Gateway := BuildRouteGateway("default", "gatewayname", "Same")
|
||||
// Create a Gateway Analyzer instance with the fake client
|
||||
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{
|
||||
&HTTPRoute,
|
||||
&Gateway,
|
||||
&Service,
|
||||
}
|
||||
|
||||
fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build()
|
||||
|
||||
analyzerInstance := HTTPRouteAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
CtrlClient: fakeClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := analyzerInstance.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var errorFound bool
|
||||
want := "HTTPRoute's backend service 'foobackend' is using port '1027' but the corresponding K8s service 'default/foobackend' isn't configured with the same port."
|
||||
for _, analysis := range analysisResults {
|
||||
for _, got := range analysis.Error {
|
||||
if want == got.Text {
|
||||
errorFound = true
|
||||
}
|
||||
}
|
||||
if errorFound {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !errorFound {
|
||||
t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want)
|
||||
}
|
||||
}
|
||||
@@ -98,26 +98,28 @@ func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
// loop over rules
|
||||
for _, rule := range ing.Spec.Rules {
|
||||
// loop over paths
|
||||
for _, path := range rule.HTTP.Paths {
|
||||
_, err := a.Client.GetClient().CoreV1().Services(ing.Namespace).Get(a.Context, path.Backend.Service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
doc := apiDoc.GetApiDocV2("spec.rules.http.paths.backend.service")
|
||||
// loop over HTTP paths
|
||||
if rule.HTTP != nil {
|
||||
for _, path := range rule.HTTP.Paths {
|
||||
_, err := a.Client.GetClient().CoreV1().Services(ing.Namespace).Get(a.Context, path.Backend.Service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
doc := apiDoc.GetApiDocV2("spec.rules.http.paths.backend.service")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Ingress uses the service %s/%s which does not exist.", ing.Namespace, path.Backend.Service.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: ing.Namespace,
|
||||
Masked: util.MaskString(ing.Namespace),
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Ingress uses the service %s/%s which does not exist.", ing.Namespace, path.Backend.Service.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: ing.Namespace,
|
||||
Masked: util.MaskString(ing.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: path.Backend.Service.Name,
|
||||
Masked: util.MaskString(path.Backend.Service.Name),
|
||||
},
|
||||
},
|
||||
{
|
||||
Unmasked: path.Backend.Service.Name,
|
||||
Masked: util.MaskString(path.Backend.Service.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,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,189 @@ 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)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 2)
|
||||
}
|
||||
igAnalyzer := IngressAnalyzer{}
|
||||
results, err := igAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
func TestIngressAnalyzerWithoutIngressClassAnnotation(t *testing.T) {
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
})
|
||||
ingressAnalyzer := IngressAnalyzer{}
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
expectations := []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/Ingress1",
|
||||
failuresCount: 1,
|
||||
},
|
||||
{
|
||||
name: "default/Ingress2",
|
||||
failuresCount: 1,
|
||||
},
|
||||
{
|
||||
name: "default/Ingress5",
|
||||
failuresCount: 4,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
analysisResults, err := ingressAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
|
||||
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")
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
119
pkg/analyzer/log.go
Normal file
119
pkg/analyzer/log.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
errorPattern = regexp.MustCompile(`(error|exception|fail)`)
|
||||
tailLines = int64(100)
|
||||
)
|
||||
|
||||
type LogAnalyzer struct {
|
||||
}
|
||||
|
||||
func (LogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "Log"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
// search all namespaces for pods that are not running
|
||||
list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
// Iterate through each pod
|
||||
|
||||
for _, pod := range list.Items {
|
||||
podName := pod.Name
|
||||
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: 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(strings.ToLower(rawlogs)) {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: printErrorLines(pod.Name, pod.Namespace, rawlogs, errorPattern),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: pod.Name,
|
||||
Masked: util.MaskString(pod.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
for key, value := range preAnalysis {
|
||||
currentAnalysis := common.Result{
|
||||
Kind: "Pod",
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
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 {
|
||||
// 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(strings.ToLower(line)) {
|
||||
return line
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
162
pkg/analyzer/mutating_webhook.go
Normal file
162
pkg/analyzer/mutating_webhook.go
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
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"
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type MutatingWebhookAnalyzer struct{}
|
||||
|
||||
func (MutatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "MutatingWebhookConfiguration"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
mutatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), v1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
for _, webhookConfig := range mutatingWebhooks.Items {
|
||||
for _, webhook := range webhookConfig.Webhooks {
|
||||
var failures []common.Failure
|
||||
|
||||
if webhook.ClientConfig.Service == nil {
|
||||
continue
|
||||
}
|
||||
svc := webhook.ClientConfig.Service
|
||||
// Get the service
|
||||
service, err := a.Client.GetClient().CoreV1().Services(svc.Namespace).Get(context.Background(), svc.Name, v1.GetOptions{})
|
||||
if err != nil {
|
||||
// If the service is not found, we can't check the pods
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Service %s not found as mapped to by Mutating Webhook %s", svc.Name, webhook.Name),
|
||||
KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: webhookConfig.Namespace,
|
||||
Masked: util.MaskString(webhookConfig.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: svc.Name,
|
||||
Masked: util.MaskString(svc.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
// When Service selectors are empty we defer to service analyser
|
||||
if len(service.Spec.Selector) == 0 {
|
||||
continue
|
||||
}
|
||||
// Get pods within service
|
||||
pods, err := a.Client.GetClient().CoreV1().Pods(svc.Namespace).List(context.Background(), v1.ListOptions{
|
||||
LabelSelector: util.MapToString(service.Spec.Selector),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pods.Items) == 0 {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("No active pods found within service %s as mapped to by Mutating Webhook %s", svc.Name, webhook.Name),
|
||||
KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: webhookConfig.Namespace,
|
||||
Masked: util.MaskString(webhookConfig.Namespace),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
for _, pod := range pods.Items {
|
||||
if pod.Status.Phase != "Running" {
|
||||
doc := apiDoc.GetApiDocV2("spec.webhook")
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf(
|
||||
"Mutating Webhook (%s) is pointing to an inactive receiver pod (%s)",
|
||||
webhook.Name,
|
||||
pod.Name,
|
||||
),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: webhookConfig.Namespace,
|
||||
Masked: util.MaskString(webhookConfig.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: webhook.Name,
|
||||
Masked: util.MaskString(webhook.Name),
|
||||
},
|
||||
{
|
||||
Unmasked: pod.Name,
|
||||
Masked: util.MaskString(pod.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
for key, value := range preAnalysis {
|
||||
var currentAnalysis = common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, found := util.GetParent(a.Client, value.MutatingWebhook.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
}
|
||||
140
pkg/analyzer/mutating_webhook_test.go
Normal file
140
pkg/analyzer/mutating_webhook_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
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))
|
||||
}
|
||||
@@ -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,7 @@ 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")
|
||||
|
||||
}
|
||||
|
||||
@@ -74,8 +74,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,155 @@ 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)
|
||||
}
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
}
|
||||
nodeAnalyzer := NodeAnalyzer{}
|
||||
var analysisResults []common.Result
|
||||
analysisResults, err := nodeAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -58,21 +63,23 @@ func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
if pdb.Spec.MinAvailable != nil {
|
||||
doc = apiDoc.GetApiDocV2("spec.minAvailable")
|
||||
}
|
||||
for k, v := range pdb.Spec.Selector.MatchLabels {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("%s, expected pdb pod label %s=%s", pdb.Status.Conditions[0].Reason, k, v),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: k,
|
||||
Masked: util.MaskString(k),
|
||||
if pdb.Spec.Selector != nil && pdb.Spec.Selector.MatchLabels != nil {
|
||||
for k, v := range pdb.Spec.Selector.MatchLabels {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("%s, expected pdb pod label %s=%s", pdb.Status.Conditions[0].Reason, k, v),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: k,
|
||||
Masked: util.MaskString(k),
|
||||
},
|
||||
{
|
||||
Unmasked: v,
|
||||
Masked: util.MaskString(v),
|
||||
},
|
||||
},
|
||||
{
|
||||
Unmasked: v,
|
||||
Masked: util.MaskString(v),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,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)
|
||||
}
|
||||
|
||||
|
||||
117
pkg/analyzer/pdb_test.go
Normal file
117
pkg/analyzer/pdb_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
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)
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -41,12 +42,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,51 +58,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 {
|
||||
if containerStatus.State.Waiting != nil {
|
||||
if containerStatus.State.Waiting.Reason == "CrashLoopBackOff" || containerStatus.State.Waiting.Reason == "ImagePullBackOff" {
|
||||
if 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" {
|
||||
// Check for errors in the init containers.
|
||||
failures = append(failures, analyzeContainerStatusFailures(a, pod.Status.InitContainerStatuses, pod.Name, pod.Namespace, string(pod.Status.Phase))...)
|
||||
|
||||
// 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{},
|
||||
})
|
||||
// Check for errors in containers.
|
||||
failures = append(failures, analyzeContainerStatusFailures(a, pod.Status.ContainerStatuses, pod.Name, pod.Namespace, string(pod.Status.Phase))...)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{
|
||||
Pod: pod,
|
||||
@@ -118,10 +80,91 @@ 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",
|
||||
}
|
||||
|
||||
for _, r := range failureReasons {
|
||||
if r == reason {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEvtErrorReason(reason string) bool {
|
||||
failureReasons := []string{
|
||||
"FailedCreatePodSandBox", "FailedMount",
|
||||
}
|
||||
|
||||
for _, r := range failureReasons {
|
||||
if r == reason {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
230
pkg/analyzer/pvc_test.go
Normal file
230
pkg/analyzer/pvc_test.go
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
146
pkg/analyzer/rs_test.go
Normal file
146
pkg/analyzer/rs_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,12 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
@@ -55,6 +57,10 @@ func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
// Check for empty service
|
||||
if len(ep.Subsets) == 0 {
|
||||
if _, ok := ep.Annotations[resourcelock.LeaderElectionRecordAnnotationKey]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
svc, err := a.Client.GetClient().CoreV1().Services(ep.Namespace).Get(a.Context, ep.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
color.Yellow("Service %s/%s does not exist", ep.Namespace, ep.Name)
|
||||
@@ -92,16 +98,18 @@ 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{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
@@ -120,8 +128,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,153 @@ 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
&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",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
serviceAnalyzer := ServiceAnalyzer{}
|
||||
analysisResults, err := serviceAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
@@ -109,8 +109,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)
|
||||
}
|
||||
|
||||
|
||||
160
pkg/analyzer/validating_webhook.go
Normal file
160
pkg/analyzer/validating_webhook.go
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
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"
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type ValidatingWebhookAnalyzer struct{}
|
||||
|
||||
func (ValidatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "ValidatingWebhookConfgiguration"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
validatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.Background(), v1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
for _, webhookConfig := range validatingWebhooks.Items {
|
||||
for _, webhook := range webhookConfig.Webhooks {
|
||||
var failures []common.Failure
|
||||
if webhook.ClientConfig.Service == nil {
|
||||
continue
|
||||
}
|
||||
svc := webhook.ClientConfig.Service
|
||||
// Get the service
|
||||
service, err := a.Client.GetClient().CoreV1().Services(svc.Namespace).Get(context.Background(), svc.Name, v1.GetOptions{})
|
||||
if err != nil {
|
||||
// If the service is not found, we can't check the pods
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Service %s not found as mapped to by Validating Webhook %s", svc.Name, webhook.Name),
|
||||
KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: webhookConfig.Namespace,
|
||||
Masked: util.MaskString(webhookConfig.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: svc.Name,
|
||||
Masked: util.MaskString(svc.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
// When Service selectors are empty we defer to service analyser
|
||||
if len(service.Spec.Selector) == 0 {
|
||||
continue
|
||||
}
|
||||
// Get pods within service
|
||||
pods, err := a.Client.GetClient().CoreV1().Pods(svc.Namespace).List(context.Background(), v1.ListOptions{
|
||||
LabelSelector: util.MapToString(service.Spec.Selector),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(pods.Items) == 0 {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("No active pods found within service %s as mapped to by Validating Webhook %s", svc.Name, webhook.Name),
|
||||
KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: webhookConfig.Namespace,
|
||||
Masked: util.MaskString(webhookConfig.Namespace),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
for _, pod := range pods.Items {
|
||||
if pod.Status.Phase != "Running" {
|
||||
doc := apiDoc.GetApiDocV2("spec.webhook")
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf(
|
||||
"Validating Webhook (%s) is pointing to an inactive receiver pod (%s)",
|
||||
webhook.Name,
|
||||
pod.Name,
|
||||
),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: webhookConfig.Namespace,
|
||||
Masked: util.MaskString(webhookConfig.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: webhook.Name,
|
||||
Masked: util.MaskString(webhook.Name),
|
||||
},
|
||||
{
|
||||
Unmasked: pod.Name,
|
||||
Masked: util.MaskString(pod.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
for key, value := range preAnalysis {
|
||||
var currentAnalysis = common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, found := util.GetParent(a.Client, value.ValidatingWebhook.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
}
|
||||
140
pkg/analyzer/validating_webhook_test.go
Normal file
140
pkg/analyzer/validating_webhook_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
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))
|
||||
}
|
||||
155
pkg/cache/azuresa_based.go
vendored
Normal file
155
pkg/cache/azuresa_based.go
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
)
|
||||
|
||||
// Generate ICache implementation
|
||||
type AzureCache struct {
|
||||
ctx context.Context
|
||||
noCache bool
|
||||
containerName string
|
||||
session *azblob.Client
|
||||
}
|
||||
|
||||
type AzureCacheConfiguration struct {
|
||||
StorageAccount string `mapstructure:"storageaccount" yaml:"storageaccount,omitempty"`
|
||||
ContainerName string `mapstructure:"container" yaml:"container,omitempty"`
|
||||
}
|
||||
|
||||
func (s *AzureCache) Configure(cacheInfo CacheProvider) error {
|
||||
s.ctx = context.Background()
|
||||
if cacheInfo.Azure.ContainerName == "" {
|
||||
log.Fatal("Azure Container name not configured")
|
||||
}
|
||||
if cacheInfo.Azure.StorageAccount == "" {
|
||||
log.Fatal("Azure Storage account not configured")
|
||||
}
|
||||
|
||||
// We assume that Storage account is already in place
|
||||
blobUrl := fmt.Sprintf("https://%s.blob.core.windows.net/", cacheInfo.Azure.StorageAccount)
|
||||
credential, err := azidentity.NewDefaultAzureCredential(nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
client, err := azblob.NewClient(blobUrl, credential, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Try to create the blob container
|
||||
_, err = client.CreateContainer(s.ctx, cacheInfo.Azure.ContainerName, nil)
|
||||
if err != nil {
|
||||
// TODO: Maybe there is a better way to check this?
|
||||
// docs: https://pkg.go.dev/github.com/Azure/azure-storage-blob-go/azblob
|
||||
if strings.Contains(err.Error(), "ContainerAlreadyExists") {
|
||||
// do nothing
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.containerName = cacheInfo.Azure.ContainerName
|
||||
s.session = client
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *AzureCache) Store(key string, data string) error {
|
||||
// Store the object as a new file in the Azure blob storage with data as the content
|
||||
cacheData := []byte(data)
|
||||
_, err := s.session.UploadBuffer(s.ctx, s.containerName, key, cacheData, &azblob.UploadBufferOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *AzureCache) Load(key string) (string, error) {
|
||||
// Load blob file contents
|
||||
load, err := s.session.DownloadStream(s.ctx, s.containerName, key, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data := bytes.Buffer{}
|
||||
retryReader := load.NewRetryReader(s.ctx, &azblob.RetryReaderOptions{})
|
||||
_, err = data.ReadFrom(retryReader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := retryReader.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return data.String(), nil
|
||||
}
|
||||
|
||||
func (s *AzureCache) List() ([]CacheObjectDetails, error) {
|
||||
// List the files in the blob containerName
|
||||
files := []CacheObjectDetails{}
|
||||
|
||||
pager := s.session.NewListBlobsFlatPager(s.containerName, &azblob.ListBlobsFlatOptions{
|
||||
Include: azblob.ListBlobsInclude{Snapshots: false, Versions: false},
|
||||
})
|
||||
|
||||
for pager.More() {
|
||||
resp, err := pager.NextPage(s.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, blob := range resp.Segment.BlobItems {
|
||||
files = append(files, CacheObjectDetails{
|
||||
Name: *blob.Name,
|
||||
UpdatedAt: *blob.Properties.LastModified,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (s *AzureCache) Remove(key string) error {
|
||||
_, err := s.session.DeleteBlob(s.ctx, s.containerName, key, &blob.DeleteOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AzureCache) Exists(key string) bool {
|
||||
// Check if the object exists in the blob storage
|
||||
pager := s.session.NewListBlobsFlatPager(s.containerName, &azblob.ListBlobsFlatOptions{
|
||||
Include: azblob.ListBlobsInclude{Snapshots: false, Versions: false},
|
||||
})
|
||||
|
||||
for pager.More() {
|
||||
resp, err := pager.NextPage(s.ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, blob := range resp.Segment.BlobItems {
|
||||
if *blob.Name == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *AzureCache) IsCacheDisabled() bool {
|
||||
return s.noCache
|
||||
}
|
||||
|
||||
func (s *AzureCache) GetName() string {
|
||||
return "azure"
|
||||
}
|
||||
|
||||
func (s *AzureCache) DisableCache() {
|
||||
s.noCache = true
|
||||
}
|
||||
127
pkg/cache/cache.go
vendored
127
pkg/cache/cache.go
vendored
@@ -1,81 +1,128 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var (
|
||||
types = []ICache{
|
||||
&AzureCache{},
|
||||
&FileBasedCache{},
|
||||
&GCSCache{},
|
||||
&S3Cache{},
|
||||
}
|
||||
)
|
||||
|
||||
type ICache interface {
|
||||
Configure(cacheInfo CacheProvider) error
|
||||
Store(key string, data string) error
|
||||
Load(key string) (string, error)
|
||||
List() ([]string, error)
|
||||
List() ([]CacheObjectDetails, error)
|
||||
Remove(key string) error
|
||||
Exists(key string) bool
|
||||
IsCacheDisabled() bool
|
||||
GetName() string
|
||||
DisableCache()
|
||||
}
|
||||
|
||||
func New(noCache bool, remoteCache bool) ICache {
|
||||
if remoteCache {
|
||||
return NewS3Cache(noCache)
|
||||
}
|
||||
return &FileBasedCache{
|
||||
noCache: noCache,
|
||||
func New(cacheType string) ICache {
|
||||
for _, t := range types {
|
||||
if cacheType == t.GetName() {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return &FileBasedCache{}
|
||||
}
|
||||
|
||||
// CacheProvider is the configuration for the cache provider when using a remote cache
|
||||
type CacheProvider struct {
|
||||
BucketName string `mapstructure:"bucketname"`
|
||||
Region string `mapstructure:"region"`
|
||||
}
|
||||
|
||||
func RemoteCacheEnabled() (bool, error) {
|
||||
// load remote cache if it is configured
|
||||
var cache CacheProvider
|
||||
err := viper.UnmarshalKey("cache", &cache)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if cache.BucketName != "" && cache.Region != "" {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func AddRemoteCache(bucketName string, region string) error {
|
||||
func ParseCacheConfiguration() (CacheProvider, error) {
|
||||
var cacheInfo CacheProvider
|
||||
err := viper.UnmarshalKey("cache", &cacheInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
return cacheInfo, err
|
||||
}
|
||||
if cacheInfo.BucketName != "" {
|
||||
return errors.New("Error: a cache is already configured, please remove it first")
|
||||
return cacheInfo, nil
|
||||
}
|
||||
|
||||
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
|
||||
case cacheType == "gcs":
|
||||
cProvider.GCS.BucketName = bucketname
|
||||
cProvider.GCS.ProjectId = projectId
|
||||
cProvider.GCS.Region = region
|
||||
case cacheType == "s3":
|
||||
cProvider.S3.BucketName = bucketname
|
||||
cProvider.S3.Region = region
|
||||
cProvider.S3.Endpoint = endpoint
|
||||
cProvider.S3.InsecureSkipVerify = insecure
|
||||
default:
|
||||
return CacheProvider{}, status.Error(codes.Internal, fmt.Sprintf("%s is not a valid option", cacheType))
|
||||
}
|
||||
cacheInfo.BucketName = bucketName
|
||||
cacheInfo.Region = region
|
||||
|
||||
cache := New(cacheType)
|
||||
err := cache.Configure(cProvider)
|
||||
if err != nil {
|
||||
return CacheProvider{}, err
|
||||
}
|
||||
return cProvider, nil
|
||||
}
|
||||
|
||||
// If we have set a remote cache, return the remote cache configuration
|
||||
func GetCacheConfiguration() (ICache, error) {
|
||||
cacheInfo, err := ParseCacheConfiguration()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cache ICache
|
||||
|
||||
switch {
|
||||
case cacheInfo.GCS != GCSCacheConfiguration{}:
|
||||
cache = &GCSCache{}
|
||||
case cacheInfo.Azure != AzureCacheConfiguration{}:
|
||||
cache = &AzureCache{}
|
||||
case cacheInfo.S3 != S3CacheConfiguration{}:
|
||||
cache = &S3Cache{}
|
||||
default:
|
||||
cache = &FileBasedCache{}
|
||||
}
|
||||
|
||||
err_config := cache.Configure(cacheInfo)
|
||||
|
||||
return cache, err_config
|
||||
}
|
||||
|
||||
func AddRemoteCache(cacheInfo CacheProvider) error {
|
||||
|
||||
viper.Set("cache", cacheInfo)
|
||||
err = viper.WriteConfig()
|
||||
|
||||
err := viper.WriteConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveRemoteCache(bucketName string) error {
|
||||
func RemoveRemoteCache() error {
|
||||
var cacheInfo CacheProvider
|
||||
err := viper.UnmarshalKey("cache", &cacheInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cacheInfo.BucketName == "" {
|
||||
return errors.New("Error: no cache is configured")
|
||||
return status.Error(codes.Internal, "cache unmarshal")
|
||||
}
|
||||
|
||||
cacheInfo = CacheProvider{}
|
||||
viper.Set("cache", cacheInfo)
|
||||
err = viper.WriteConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
return status.Error(codes.Internal, "unable to write config")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
39
pkg/cache/file_based.go
vendored
39
pkg/cache/file_based.go
vendored
@@ -15,11 +15,15 @@ type FileBasedCache struct {
|
||||
noCache bool
|
||||
}
|
||||
|
||||
func (f *FileBasedCache) Configure(cacheInfo CacheProvider) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileBasedCache) IsCacheDisabled() bool {
|
||||
return f.noCache
|
||||
}
|
||||
|
||||
func (*FileBasedCache) List() ([]string, error) {
|
||||
func (*FileBasedCache) List() ([]CacheObjectDetails, error) {
|
||||
path, err := xdg.CacheFile("k8sgpt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -30,9 +34,16 @@ func (*FileBasedCache) List() ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []string
|
||||
var result []CacheObjectDetails
|
||||
for _, file := range files {
|
||||
result = append(result, file.Name())
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, CacheObjectDetails{
|
||||
Name: file.Name(),
|
||||
UpdatedAt: info.ModTime(),
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -72,6 +83,20 @@ func (*FileBasedCache) Load(key string) (string, error) {
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (*FileBasedCache) Remove(key string) error {
|
||||
path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*FileBasedCache) Store(key string, data string) error {
|
||||
path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
|
||||
|
||||
@@ -81,3 +106,11 @@ func (*FileBasedCache) Store(key string, data string) error {
|
||||
|
||||
return os.WriteFile(path, []byte(data), 0600)
|
||||
}
|
||||
|
||||
func (s *FileBasedCache) GetName() string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
func (s *FileBasedCache) DisableCache() {
|
||||
s.noCache = true
|
||||
}
|
||||
|
||||
133
pkg/cache/gcs_based.go
vendored
Normal file
133
pkg/cache/gcs_based.go
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
type GCSCache struct {
|
||||
ctx context.Context
|
||||
noCache bool
|
||||
bucketName string
|
||||
projectId string
|
||||
region string
|
||||
session *storage.Client
|
||||
}
|
||||
|
||||
type GCSCacheConfiguration struct {
|
||||
ProjectId string `mapstructure:"projectid" yaml:"projectid,omitempty"`
|
||||
Region string `mapstructure:"region" yaml:"region,omitempty"`
|
||||
BucketName string `mapstructure:"bucketname" yaml:"bucketname,omitempty"`
|
||||
}
|
||||
|
||||
func (s *GCSCache) Configure(cacheInfo CacheProvider) error {
|
||||
s.ctx = context.Background()
|
||||
if cacheInfo.GCS.BucketName == "" {
|
||||
log.Fatal("Bucket name not configured")
|
||||
}
|
||||
if cacheInfo.GCS.Region == "" {
|
||||
log.Fatal("Region not configured")
|
||||
}
|
||||
if cacheInfo.GCS.ProjectId == "" {
|
||||
log.Fatal("ProjectID not configured")
|
||||
}
|
||||
s.bucketName = cacheInfo.GCS.BucketName
|
||||
s.projectId = cacheInfo.GCS.ProjectId
|
||||
s.region = cacheInfo.GCS.Region
|
||||
storageClient, err := storage.NewClient(s.ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = storageClient.Bucket(s.bucketName).Attrs(s.ctx)
|
||||
if err == storage.ErrBucketNotExist {
|
||||
err = storageClient.Bucket(s.bucketName).Create(s.ctx, s.projectId, &storage.BucketAttrs{
|
||||
Location: s.region,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.session = storageClient
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GCSCache) Store(key string, data string) error {
|
||||
wc := s.session.Bucket(s.bucketName).Object(key).NewWriter(s.ctx)
|
||||
|
||||
if _, err := wc.Write([]byte(data)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := wc.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GCSCache) Load(key string) (string, error) {
|
||||
reader, err := s.session.Bucket(s.bucketName).Object(key).NewReader(s.ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (s *GCSCache) Remove(key string) error {
|
||||
bucketClient := s.session.Bucket(s.bucketName)
|
||||
obj := bucketClient.Object(key)
|
||||
if err := obj.Delete(s.ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *GCSCache) List() ([]CacheObjectDetails, error) {
|
||||
var files []CacheObjectDetails
|
||||
|
||||
items := s.session.Bucket(s.bucketName).Objects(s.ctx, nil)
|
||||
for {
|
||||
attrs, err := items.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, CacheObjectDetails{
|
||||
Name: attrs.Name,
|
||||
UpdatedAt: attrs.Updated,
|
||||
})
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (s *GCSCache) Exists(key string) bool {
|
||||
obj := s.session.Bucket(s.bucketName).Object(key)
|
||||
_, err := obj.Attrs(s.ctx)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *GCSCache) IsCacheDisabled() bool {
|
||||
return s.noCache
|
||||
}
|
||||
|
||||
func (s *GCSCache) GetName() string {
|
||||
return "gcs"
|
||||
}
|
||||
|
||||
func (s *GCSCache) DisableCache() {
|
||||
s.noCache = true
|
||||
}
|
||||
123
pkg/cache/s3_based.go
vendored
123
pkg/cache/s3_based.go
vendored
@@ -2,11 +2,13 @@ package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Generate ICache implementation
|
||||
@@ -16,6 +18,53 @@ type S3Cache struct {
|
||||
session *s3.S3
|
||||
}
|
||||
|
||||
type S3CacheConfiguration struct {
|
||||
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")
|
||||
}
|
||||
s.bucketName = cacheInfo.S3.BucketName
|
||||
|
||||
sess := session.Must(session.NewSessionWithOptions(session.Options{
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
Config: aws.Config{
|
||||
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)
|
||||
|
||||
// Check if the bucket exists, if not create it
|
||||
_, err := s3Client.HeadBucket(&s3.HeadBucketInput{
|
||||
Bucket: aws.String(cacheInfo.S3.BucketName),
|
||||
})
|
||||
if err != nil {
|
||||
_, err = s3Client.CreateBucket(&s3.CreateBucketInput{
|
||||
Bucket: aws.String(cacheInfo.S3.BucketName),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.session = s3Client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Cache) Store(key string, data string) error {
|
||||
// Store the object as a new file in the bucket with data as the content
|
||||
_, err := s.session.PutObject(&s3.PutObjectInput{
|
||||
@@ -27,6 +76,18 @@ func (s *S3Cache) Store(key string, data string) error {
|
||||
|
||||
}
|
||||
|
||||
func (s *S3Cache) Remove(key string) error {
|
||||
_, err := s.session.DeleteObject(&s3.DeleteObjectInput{
|
||||
Bucket: &s.bucketName,
|
||||
Key: aws.String(key),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Cache) Load(key string) (string, error) {
|
||||
|
||||
// Retrieve the object from the bucket and load it into a string
|
||||
@@ -39,12 +100,12 @@ 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() ([]string, error) {
|
||||
func (s *S3Cache) List() ([]CacheObjectDetails, error) {
|
||||
|
||||
// List the files in the bucket
|
||||
result, err := s.session.ListObjectsV2(&s3.ListObjectsV2Input{Bucket: aws.String(s.bucketName)})
|
||||
@@ -52,9 +113,12 @@ func (s *S3Cache) List() ([]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var keys []string
|
||||
var keys []CacheObjectDetails
|
||||
for _, item := range result.Contents {
|
||||
keys = append(keys, *item.Key)
|
||||
keys = append(keys, CacheObjectDetails{
|
||||
Name: *item.Key,
|
||||
UpdatedAt: *item.LastModified,
|
||||
})
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
@@ -74,45 +138,10 @@ func (s *S3Cache) IsCacheDisabled() bool {
|
||||
return s.noCache
|
||||
}
|
||||
|
||||
func NewS3Cache(nocache bool) ICache {
|
||||
|
||||
var cache CacheProvider
|
||||
err := viper.UnmarshalKey("cache", &cache)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if cache.BucketName == "" {
|
||||
panic("Bucket name not configured")
|
||||
}
|
||||
if cache.Region == "" {
|
||||
panic("Region not configured")
|
||||
}
|
||||
|
||||
sess := session.Must(session.NewSessionWithOptions(session.Options{
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
Config: aws.Config{
|
||||
Region: aws.String(cache.Region),
|
||||
},
|
||||
}))
|
||||
|
||||
s := s3.New(sess)
|
||||
|
||||
// Check if the bucket exists, if not create it
|
||||
_, err = s.HeadBucket(&s3.HeadBucketInput{
|
||||
Bucket: aws.String(cache.BucketName),
|
||||
})
|
||||
if err != nil {
|
||||
_, err = s.CreateBucket(&s3.CreateBucketInput{
|
||||
Bucket: aws.String(cache.BucketName),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return &S3Cache{
|
||||
noCache: nocache,
|
||||
session: s,
|
||||
bucketName: cache.BucketName,
|
||||
}
|
||||
func (s *S3Cache) GetName() string {
|
||||
return "s3"
|
||||
}
|
||||
|
||||
func (s *S3Cache) DisableCache() {
|
||||
s.noCache = true
|
||||
}
|
||||
|
||||
14
pkg/cache/types.go
vendored
Normal file
14
pkg/cache/types.go
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
type CacheObjectDetails struct {
|
||||
Name string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -20,11 +20,14 @@ import (
|
||||
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"
|
||||
regv1 "k8s.io/api/admissionregistration/v1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autov1 "k8s.io/api/autoscaling/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
networkv1 "k8s.io/api/networking/v1"
|
||||
policyv1 "k8s.io/api/policy/v1"
|
||||
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
type IAnalyzer interface {
|
||||
@@ -54,8 +57,15 @@ type PreAnalysis struct {
|
||||
StatefulSet appsv1.StatefulSet
|
||||
NetworkPolicy networkv1.NetworkPolicy
|
||||
Node v1.Node
|
||||
ValidatingWebhook regv1.ValidatingWebhookConfiguration
|
||||
MutatingWebhook regv1.MutatingWebhookConfiguration
|
||||
GatewayClass gtwapi.GatewayClass
|
||||
Gateway gtwapi.Gateway
|
||||
HTTPRoute gtwapi.HTTPRoute
|
||||
// Integrations
|
||||
ScaledObject keda.ScaledObject
|
||||
TrivyVulnerabilityReport trivy.VulnerabilityReport
|
||||
TrivyConfigAuditReport trivy.ConfigAuditReport
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
|
||||
57
pkg/custom/client.go
Normal file
57
pkg/custom/client.go
Normal file
@@ -0,0 +1,57 @@
|
||||
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.AnalyzerServiceClient
|
||||
}
|
||||
|
||||
func NewClient(c Connection) (*Client, error) {
|
||||
|
||||
conn, err := grpc.Dial(fmt.Sprintf("%s:%s", c.Url, c.Port), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := rpc.NewAnalyzerServiceClient(conn)
|
||||
return &Client{
|
||||
c: conn,
|
||||
analyzerClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cli *Client) Run() (common.Result, error) {
|
||||
var result common.Result
|
||||
req := &schemav1.AnalyzerRunRequest{}
|
||||
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
|
||||
}
|
||||
10
pkg/custom/types.go
Normal file
10
pkg/custom/types.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package custom
|
||||
|
||||
type Connection struct {
|
||||
Url string `json:"url"`
|
||||
Port string `json:"port"`
|
||||
}
|
||||
type CustomAnalyzer struct {
|
||||
Name string `json:"name"`
|
||||
Connection Connection `json:"connection"`
|
||||
}
|
||||
85
pkg/integration/aws/aws.go
Normal file
85
pkg/integration/aws/aws.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/spf13/viper"
|
||||
"os"
|
||||
)
|
||||
|
||||
type AWS struct {
|
||||
sess *session.Session
|
||||
}
|
||||
|
||||
func (a *AWS) Deploy(namespace string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AWS) UnDeploy(namespace string) error {
|
||||
a.sess = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AWS) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) {
|
||||
// Check for AWS credentials in the environment
|
||||
// https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
|
||||
if os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
|
||||
panic("AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set in the environment")
|
||||
}
|
||||
|
||||
sess := session.Must(session.NewSessionWithOptions(session.Options{
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
Config: aws.Config{},
|
||||
}))
|
||||
a.sess = sess
|
||||
(*mergedMap)["EKS"] = &EKSAnalyzer{
|
||||
session: a.sess,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AWS) GetAnalyzerName() []string {
|
||||
|
||||
return []string{"EKS"}
|
||||
}
|
||||
|
||||
func (a *AWS) GetNamespace() (string, error) {
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (a *AWS) OwnsAnalyzer(s string) bool {
|
||||
for _, az := range a.GetAnalyzerName() {
|
||||
if s == az {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AWS) isFilterActive() bool {
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
for _, filter := range a.GetAnalyzerName() {
|
||||
for _, af := range activeFilters {
|
||||
if af == filter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AWS) IsActivate() bool {
|
||||
if a.isFilterActive() {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func NewAWS() *AWS {
|
||||
return &AWS{}
|
||||
}
|
||||
80
pkg/integration/aws/eks.go
Normal file
80
pkg/integration/aws/eks.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/spf13/viper"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/eks"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
type EKSAnalyzer struct {
|
||||
session *session.Session
|
||||
}
|
||||
|
||||
func (e *EKSAnalyzer) Analyze(analysis common.Analyzer) ([]common.Result, error) {
|
||||
var cr []common.Result = []common.Result{}
|
||||
_ = map[string]common.PreAnalysis{}
|
||||
svc := eks.New(e.session)
|
||||
// Get the name of the current cluster
|
||||
var kubeconfig string
|
||||
kubeconfigFromPath := viper.GetString("kubeconfig")
|
||||
if kubeconfigFromPath != "" {
|
||||
kubeconfig = kubeconfigFromPath
|
||||
} else {
|
||||
kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube", "config")
|
||||
}
|
||||
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
||||
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
|
||||
&clientcmd.ConfigOverrides{
|
||||
CurrentContext: "",
|
||||
}).RawConfig()
|
||||
if err != nil {
|
||||
return cr, err
|
||||
}
|
||||
currentConfig := config.CurrentContext
|
||||
|
||||
if !strings.Contains(currentConfig, "eks") {
|
||||
return cr, errors.New("EKS cluster was not detected")
|
||||
}
|
||||
|
||||
input := &eks.ListClustersInput{}
|
||||
result, err := svc.ListClusters(input)
|
||||
if err != nil {
|
||||
return cr, err
|
||||
}
|
||||
for _, cluster := range result.Clusters {
|
||||
// describe the cluster
|
||||
if !strings.Contains(currentConfig, *cluster) {
|
||||
continue
|
||||
}
|
||||
input := &eks.DescribeClusterInput{
|
||||
Name: cluster,
|
||||
}
|
||||
result, err := svc.DescribeCluster(input)
|
||||
if err != nil {
|
||||
return cr, err
|
||||
}
|
||||
if len(result.Cluster.Health.Issues) > 0 {
|
||||
for _, issue := range result.Cluster.Health.Issues {
|
||||
err := make([]common.Failure, 0)
|
||||
err = append(err, common.Failure{
|
||||
Text: issue.String(),
|
||||
KubernetesDoc: "",
|
||||
Sensitive: nil,
|
||||
})
|
||||
cr = append(cr, common.Result{
|
||||
Kind: "EKS",
|
||||
Name: "AWS/EKS",
|
||||
Error: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return cr, nil
|
||||
}
|
||||
@@ -15,11 +15,13 @@ package integration
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/integration/aws"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/integration/keda"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/integration/prometheus"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/integration/trivy"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"github.com/spf13/viper"
|
||||
@@ -32,10 +34,12 @@ type IIntegration interface {
|
||||
UnDeploy(namespace string) error
|
||||
//
|
||||
AddAnalyzer(*map[string]common.IAnalyzer)
|
||||
// RemoveAnalyzer removes an analyzer from the cluster
|
||||
RemoveAnalyzer() error
|
||||
|
||||
GetAnalyzerName() string
|
||||
GetAnalyzerName() []string
|
||||
// An integration must keep record of its deployed namespace (if not using --no-install)
|
||||
GetNamespace() (string, error)
|
||||
|
||||
OwnsAnalyzer(string) bool
|
||||
|
||||
IsActivate() bool
|
||||
}
|
||||
@@ -44,7 +48,10 @@ type Integration struct {
|
||||
}
|
||||
|
||||
var integrations = map[string]IIntegration{
|
||||
"trivy": trivy.NewTrivy(),
|
||||
"trivy": trivy.NewTrivy(),
|
||||
"prometheus": prometheus.NewPrometheus(),
|
||||
"aws": aws.NewAWS(),
|
||||
"keda": keda.NewKeda(),
|
||||
}
|
||||
|
||||
func NewIntegration() *Integration {
|
||||
@@ -66,30 +73,37 @@ func (*Integration) Get(name string) (IIntegration, error) {
|
||||
return integrations[name], nil
|
||||
}
|
||||
|
||||
func (*Integration) Activate(name string, namespace string, activeFilters []string) error {
|
||||
func (i *Integration) AnalyzerByIntegration(input string) (string, error) {
|
||||
|
||||
for _, name := range i.List() {
|
||||
if integ, err := i.Get(name); err == nil {
|
||||
if integ.OwnsAnalyzer(input) {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("analyzerbyintegration: no matches found")
|
||||
}
|
||||
|
||||
func (*Integration) Activate(name string, namespace string, activeFilters []string, skipInstall bool) error {
|
||||
if _, ok := integrations[name]; !ok {
|
||||
return errors.New("integration not found")
|
||||
}
|
||||
|
||||
mergedFilters := append(activeFilters, integrations[name].GetAnalyzerName())
|
||||
|
||||
uniqueFilters, dupplicatedFilters := util.RemoveDuplicates(mergedFilters)
|
||||
|
||||
// Verify dupplicate
|
||||
if len(dupplicatedFilters) != 0 {
|
||||
color.Red("Integration already activated : %s", strings.Join(dupplicatedFilters, ", "))
|
||||
os.Exit(1)
|
||||
if !skipInstall {
|
||||
if err := integrations[name].Deploy(namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
mergedFilters := activeFilters
|
||||
mergedFilters = append(mergedFilters, integrations[name].GetAnalyzerName()...)
|
||||
uniqueFilters, _ := util.RemoveDuplicates(mergedFilters)
|
||||
|
||||
viper.Set("active_filters", uniqueFilters)
|
||||
|
||||
if err := integrations[name].Deploy(namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
color.Red("Error writing config file: %s", err.Error())
|
||||
os.Exit(1)
|
||||
return fmt.Errorf("error writing config file: %s", err.Error())
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -102,19 +116,14 @@ func (*Integration) Deactivate(name string, namespace string) error {
|
||||
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
// Update filters
|
||||
// This might be a bad idea, but we cannot reference analyzer here
|
||||
foundFilter := false
|
||||
for i, v := range activeFilters {
|
||||
if v == integrations[name].GetAnalyzerName() {
|
||||
foundFilter = true
|
||||
activeFilters = append(activeFilters[:i], activeFilters[i+1:]...)
|
||||
break
|
||||
// Update filters and remove the specific filters for the integration
|
||||
for _, filter := range integrations[name].GetAnalyzerName() {
|
||||
for x, af := range activeFilters {
|
||||
if af == filter {
|
||||
activeFilters = append(activeFilters[:x], activeFilters[x+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundFilter {
|
||||
color.Red("Ingregation %s does not exist in configuration file. Please use k8sgpt integration add.", name)
|
||||
os.Exit(1)
|
||||
|
||||
}
|
||||
|
||||
if err := integrations[name].UnDeploy(namespace); err != nil {
|
||||
@@ -124,8 +133,8 @@ func (*Integration) Deactivate(name string, namespace string) error {
|
||||
viper.Set("active_filters", activeFilters)
|
||||
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
color.Red("Error writing config file: %s", err.Error())
|
||||
os.Exit(1)
|
||||
return fmt.Errorf("error writing config file: %s", err.Error())
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
229
pkg/integration/keda/keda.go
Normal file
229
pkg/integration/keda/keda.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package keda
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/typed/keda/v1alpha1"
|
||||
helmclient "github.com/mittwald/go-helm-client"
|
||||
"github.com/spf13/viper"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
)
|
||||
|
||||
var (
|
||||
Repo = getEnv("KEDA_REPO", "https://kedacore.github.io/charts")
|
||||
Version = getEnv("KEDA_VERSION", "2.11.2")
|
||||
ChartName = getEnv("KEDA_CHART_NAME", "keda")
|
||||
RepoShortName = getEnv("KEDA_REPO_SHORT_NAME", "keda")
|
||||
ReleaseName = getEnv("KEDA_RELEASE_NAME", "keda-k8sgpt")
|
||||
)
|
||||
|
||||
type Keda struct {
|
||||
helm helmclient.Client
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func NewKeda() *Keda {
|
||||
helmClient, err := helmclient.New(&helmclient.Options{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &Keda{
|
||||
helm: helmClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Keda) Deploy(namespace string) error {
|
||||
// Add the repository
|
||||
chartRepo := repo.Entry{
|
||||
Name: RepoShortName,
|
||||
URL: Repo,
|
||||
}
|
||||
// Add a chart-repository to the client.
|
||||
if err := k.helm.AddOrUpdateChartRepo(chartRepo); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
chartSpec := helmclient.ChartSpec{
|
||||
ReleaseName: ReleaseName,
|
||||
ChartName: fmt.Sprintf("%s/%s", RepoShortName, ChartName),
|
||||
Namespace: namespace,
|
||||
|
||||
//TODO: All of this should be configurable
|
||||
UpgradeCRDs: true,
|
||||
Wait: false,
|
||||
Timeout: 300,
|
||||
CreateNamespace: true,
|
||||
}
|
||||
|
||||
// Install a chart release.
|
||||
// Note that helmclient.Options.Namespace should ideally match the namespace in chartSpec.Namespace.
|
||||
if _, err := k.helm.InstallOrUpgradeChart(context.Background(), &chartSpec, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Keda) UnDeploy(namespace string) error {
|
||||
kubecontext := viper.GetString("kubecontext")
|
||||
kubeconfig := viper.GetString("kubeconfig")
|
||||
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
|
||||
if err != nil {
|
||||
// TODO: better error handling
|
||||
color.Red("Error initialising kubernetes client: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
kedaNamespace, _ := k.GetNamespace()
|
||||
color.Blue(fmt.Sprintf("Keda namespace: %s\n", kedaNamespace))
|
||||
|
||||
kClient, _ := v1alpha1.NewForConfig(client.Config)
|
||||
|
||||
scaledObjectList, _ := kClient.ScaledObjects("").List(context.Background(), v1.ListOptions{})
|
||||
scaledJobList, _ := kClient.ScaledJobs("").List(context.Background(), v1.ListOptions{})
|
||||
triggerAuthenticationList, _ := kClient.TriggerAuthentications("").List(context.Background(), v1.ListOptions{})
|
||||
clusterTriggerAuthenticationsList, _ := kClient.ClusterTriggerAuthentications().List(context.Background(), v1.ListOptions{})
|
||||
|
||||
// Before uninstalling the Helm chart, we need to delete Keda resources
|
||||
for _, scaledObject := range scaledObjectList.Items {
|
||||
err := kClient.ScaledObjects(scaledObject.Namespace).Delete(context.Background(), scaledObject.Name, v1.DeleteOptions{})
|
||||
if err != nil {
|
||||
fmt.Printf("Error deleting scaledObject %s: %v\n", scaledObject.Name, err)
|
||||
} else {
|
||||
fmt.Printf("Deleted scaledObject %s in namespace %s\n", scaledObject.Name, scaledObject.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
for _, scaledJob := range scaledJobList.Items {
|
||||
err := kClient.ScaledJobs(scaledJob.Namespace).Delete(context.Background(), scaledJob.Name, v1.DeleteOptions{})
|
||||
if err != nil {
|
||||
fmt.Printf("Error deleting scaledJob %s: %v\n", scaledJob.Name, err)
|
||||
} else {
|
||||
fmt.Printf("Deleted scaledJob %s in namespace %s\n", scaledJob.Name, scaledJob.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
for _, triggerAuthentication := range triggerAuthenticationList.Items {
|
||||
err := kClient.TriggerAuthentications(triggerAuthentication.Namespace).Delete(context.Background(), triggerAuthentication.Name, v1.DeleteOptions{})
|
||||
if err != nil {
|
||||
fmt.Printf("Error deleting triggerAuthentication %s: %v\n", triggerAuthentication.Name, err)
|
||||
} else {
|
||||
fmt.Printf("Deleted triggerAuthentication %s in namespace %s\n", triggerAuthentication.Name, triggerAuthentication.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
for _, clusterTriggerAuthentication := range clusterTriggerAuthenticationsList.Items {
|
||||
err := kClient.ClusterTriggerAuthentications().Delete(context.Background(), clusterTriggerAuthentication.Name, v1.DeleteOptions{})
|
||||
if err != nil {
|
||||
fmt.Printf("Error deleting clusterTriggerAuthentication %s: %v\n", clusterTriggerAuthentication.Name, err)
|
||||
} else {
|
||||
fmt.Printf("Deleted clusterTriggerAuthentication %s\n", clusterTriggerAuthentication.Name)
|
||||
}
|
||||
}
|
||||
|
||||
chartSpec := helmclient.ChartSpec{
|
||||
ReleaseName: ReleaseName,
|
||||
ChartName: fmt.Sprintf("%s/%s", RepoShortName, ChartName),
|
||||
Namespace: namespace,
|
||||
UpgradeCRDs: true,
|
||||
Wait: false,
|
||||
Timeout: 300,
|
||||
}
|
||||
// Uninstall the chart release.
|
||||
// Note that helmclient.Options.Namespace should ideally match the namespace in chartSpec.Namespace.
|
||||
if err := k.helm.UninstallRelease(&chartSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Keda) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) {
|
||||
(*mergedMap)["ScaledObject"] = &ScaledObjectAnalyzer{}
|
||||
}
|
||||
|
||||
func (k *Keda) GetAnalyzerName() []string {
|
||||
return []string{
|
||||
"ScaledObject",
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Keda) GetNamespace() (string, error) {
|
||||
releases, err := k.helm.ListDeployedReleases()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, rel := range releases {
|
||||
if rel.Name == ReleaseName {
|
||||
return rel.Namespace, nil
|
||||
}
|
||||
}
|
||||
return "", status.Error(codes.NotFound, "keda release not found")
|
||||
}
|
||||
|
||||
func (k *Keda) OwnsAnalyzer(analyzer string) bool {
|
||||
for _, a := range k.GetAnalyzerName() {
|
||||
if analyzer == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *Keda) isFilterActive() bool {
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
for _, filter := range k.GetAnalyzerName() {
|
||||
for _, af := range activeFilters {
|
||||
if af == filter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *Keda) isDeployed() bool {
|
||||
kubecontext := viper.GetString("kubecontext")
|
||||
kubeconfig := viper.GetString("kubeconfig")
|
||||
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
|
||||
if err != nil {
|
||||
// TODO: better error handling
|
||||
color.Red("Error initialising kubernetes client: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
groups, _, err := client.Client.Discovery().ServerGroupsAndResources()
|
||||
if err != nil {
|
||||
// TODO: better error handling
|
||||
color.Red("Error initialising discovery client: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
if group.Name == "keda.sh" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *Keda) IsActivate() bool {
|
||||
return k.isFilterActive() && k.isDeployed()
|
||||
}
|
||||
193
pkg/integration/keda/scaledobject_analyzer.go
Normal file
193
pkg/integration/keda/scaledobject_analyzer.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package keda
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
kedaSchema "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
|
||||
"github.com/kedacore/keda/v2/pkg/generated/clientset/versioned/typed/keda/v1alpha1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type ScaledObjectAnalyzer struct{}
|
||||
|
||||
func (s *ScaledObjectAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
kClient, _ := v1alpha1.NewForConfig(a.Client.GetConfig())
|
||||
kind := "ScaledObject"
|
||||
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: kedaSchema.GroupVersion,
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
list, err := kClient.ScaledObjects(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
for _, so := range list.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
scaleTargetRef := so.Spec.ScaleTargetRef
|
||||
if scaleTargetRef.Kind == "" {
|
||||
scaleTargetRef.Kind = "Deployment"
|
||||
}
|
||||
|
||||
var podInfo PodInfo
|
||||
|
||||
switch scaleTargetRef.Kind {
|
||||
case "Deployment":
|
||||
deployment, err := a.Client.GetClient().AppsV1().Deployments(so.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
podInfo = DeploymentInfo{deployment}
|
||||
}
|
||||
case "ReplicationController":
|
||||
rc, err := a.Client.GetClient().CoreV1().ReplicationControllers(so.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
podInfo = ReplicationControllerInfo{rc}
|
||||
}
|
||||
case "ReplicaSet":
|
||||
rs, err := a.Client.GetClient().AppsV1().ReplicaSets(so.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
podInfo = ReplicaSetInfo{rs}
|
||||
}
|
||||
case "StatefulSet":
|
||||
ss, err := a.Client.GetClient().AppsV1().StatefulSets(so.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
podInfo = StatefulSetInfo{ss}
|
||||
}
|
||||
default:
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("ScaledObject uses %s as ScaleTargetRef which is not an option.", scaleTargetRef.Kind),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
|
||||
if podInfo == nil {
|
||||
doc := apiDoc.GetApiDocV2("spec.scaleTargetRef")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("ScaledObject uses %s/%s as ScaleTargetRef which does not exist.", scaleTargetRef.Kind, scaleTargetRef.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: scaleTargetRef.Name,
|
||||
Masked: util.MaskString(scaleTargetRef.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
containers := len(podInfo.GetPodSpec().Containers)
|
||||
for _, container := range podInfo.GetPodSpec().Containers {
|
||||
for _, trigger := range so.Spec.Triggers {
|
||||
if trigger.Type == "cpu" || trigger.Type == "memory" {
|
||||
if container.Resources.Requests == nil || container.Resources.Limits == nil {
|
||||
containers--
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if containers <= 0 {
|
||||
doc := apiDoc.GetApiDocV2("spec.scaleTargetRef.kind")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("%s %s/%s does not have resource configured.", scaleTargetRef.Kind, so.Namespace, scaleTargetRef.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: scaleTargetRef.Name,
|
||||
Masked: util.MaskString(scaleTargetRef.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
evt, err := util.FetchLatestEvent(a.Context, a.Client, so.Namespace, so.Name)
|
||||
if err != nil || evt == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if evt.Type != "Normal" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: evt.Message,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: scaleTargetRef.Name,
|
||||
Masked: util.MaskString(scaleTargetRef.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", so.Namespace, so.Name)] = common.PreAnalysis{
|
||||
ScaledObject: so,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range preAnalysis {
|
||||
var currentAnalysis = common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.ScaledObject.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
}
|
||||
|
||||
type PodInfo interface {
|
||||
GetPodSpec() corev1.PodSpec
|
||||
}
|
||||
|
||||
type DeploymentInfo struct {
|
||||
*appsv1.Deployment
|
||||
}
|
||||
|
||||
func (d DeploymentInfo) GetPodSpec() corev1.PodSpec {
|
||||
return d.Spec.Template.Spec
|
||||
}
|
||||
|
||||
// define a structure for ReplicationController
|
||||
type ReplicationControllerInfo struct {
|
||||
*corev1.ReplicationController
|
||||
}
|
||||
|
||||
func (rc ReplicationControllerInfo) GetPodSpec() corev1.PodSpec {
|
||||
return rc.Spec.Template.Spec
|
||||
}
|
||||
|
||||
// define a structure for ReplicaSet
|
||||
type ReplicaSetInfo struct {
|
||||
*appsv1.ReplicaSet
|
||||
}
|
||||
|
||||
func (rs ReplicaSetInfo) GetPodSpec() corev1.PodSpec {
|
||||
return rs.Spec.Template.Spec
|
||||
}
|
||||
|
||||
// define a structure for StatefulSet
|
||||
type StatefulSetInfo struct {
|
||||
*appsv1.StatefulSet
|
||||
}
|
||||
|
||||
// implement PodInfo for StatefulSetInfo
|
||||
func (ss StatefulSetInfo) GetPodSpec() corev1.PodSpec {
|
||||
return ss.Spec.Template.Spec
|
||||
}
|
||||
290
pkg/integration/prometheus/config_analyzer.go
Normal file
290
pkg/integration/prometheus/config_analyzer.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
promconfig "github.com/prometheus/prometheus/config"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
prometheusContainerName = "prometheus"
|
||||
configReloaderContainerName = "config-reloader"
|
||||
prometheusConfigFlag = "--config.file="
|
||||
configReloaderConfigFlag = "--config-file="
|
||||
)
|
||||
|
||||
var prometheusPodLabels = map[string]string{
|
||||
"app": "prometheus",
|
||||
"app.kubernetes.io/name": "prometheus",
|
||||
}
|
||||
|
||||
type ConfigAnalyzer struct {
|
||||
}
|
||||
|
||||
// podConfig groups a specific pod with the Prometheus configuration and any
|
||||
// other state used for informing the common.Result.
|
||||
type podConfig struct {
|
||||
b []byte
|
||||
pod *corev1.Pod
|
||||
}
|
||||
|
||||
func (c *ConfigAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
ctx := a.Context
|
||||
client := a.Client.GetClient()
|
||||
namespace := a.Namespace
|
||||
kind := ConfigValidate
|
||||
|
||||
podConfigs, err := findPrometheusPodConfigs(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
for _, pc := range podConfigs {
|
||||
var failures []common.Failure
|
||||
pod := pc.pod
|
||||
|
||||
// Check upstream validation.
|
||||
// The Prometheus configuration structs do not generally have validation
|
||||
// methods and embed their validation logic in the UnmarshalYAML methods.
|
||||
config, err := unmarshalPromConfigBytes(pc.b)
|
||||
if err != nil {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("error validating Prometheus YAML configuration: %s", err),
|
||||
})
|
||||
}
|
||||
_, err = yaml.Marshal(config)
|
||||
if err != nil {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("error validating Prometheus struct configuration: %s", err),
|
||||
})
|
||||
}
|
||||
|
||||
// Check for empty scrape config.
|
||||
if len(config.ScrapeConfigs) == 0 {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: "no scrape configurations. Prometheus will not scrape any metrics.",
|
||||
})
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{
|
||||
Pod: *pod,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range preAnalysis {
|
||||
var currentAnalysis = common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
parent, _ := util.GetParent(a.Client, value.Pod.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
}
|
||||
|
||||
func configKey(namespace string, volume *corev1.Volume) (string, error) {
|
||||
if volume.ConfigMap != nil {
|
||||
return fmt.Sprintf("configmap/%s/%s", namespace, volume.ConfigMap.Name), nil
|
||||
} else if volume.Secret != nil {
|
||||
return fmt.Sprintf("secret/%s/%s", namespace, volume.Secret.SecretName), nil
|
||||
} else {
|
||||
return "", errors.New("volume format must be ConfigMap or Secret")
|
||||
}
|
||||
}
|
||||
|
||||
func findPrometheusPodConfigs(ctx context.Context, client kubernetes.Interface, namespace string) ([]podConfig, error) {
|
||||
var configs []podConfig
|
||||
pods, err := findPrometheusPods(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var configCache = make(map[string]bool)
|
||||
|
||||
for _, pod := range pods {
|
||||
// Extract volume of Prometheus config.
|
||||
volume, key, err := findPrometheusConfigVolumeAndKey(ctx, client, &pod)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// See if we processed it already; if so, don't process again.
|
||||
ck, err := configKey(pod.Namespace, volume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, ok := configCache[ck]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
configCache[ck] = true
|
||||
|
||||
// Extract Prometheus config bytes from volume.
|
||||
b, err := extractPrometheusConfigFromVolume(ctx, client, volume, pod.Namespace, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs = append(configs, podConfig{
|
||||
pod: &pod,
|
||||
b: b,
|
||||
})
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func findPrometheusPods(ctx context.Context, client kubernetes.Interface, namespace string) ([]corev1.Pod, error) {
|
||||
var proms []corev1.Pod
|
||||
for k, v := range prometheusPodLabels {
|
||||
pods, err := util.GetPodListByLabels(client, namespace, map[string]string{
|
||||
k: v,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proms = append(proms, pods.Items...)
|
||||
}
|
||||
|
||||
// If we still haven't found any Prometheus pods, make a last-ditch effort to
|
||||
// scrape the namespace for "prometheus" containers.
|
||||
if len(proms) == 0 {
|
||||
pods, err := client.CoreV1().Pods(namespace).List(ctx, v1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, pod := range pods.Items {
|
||||
for _, c := range pod.Spec.Containers {
|
||||
if c.Name == prometheusContainerName {
|
||||
proms = append(proms, pod)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return proms, nil
|
||||
}
|
||||
|
||||
func findPrometheusConfigPath(ctx context.Context, client kubernetes.Interface, pod *corev1.Pod) (string, error) {
|
||||
var path string
|
||||
var err error
|
||||
for _, container := range pod.Spec.Containers {
|
||||
for _, arg := range container.Args {
|
||||
// Prefer the config-reloader container config file as it normally
|
||||
// references the ConfigMap or Secret volume mount.
|
||||
// Fallback to the prometheus container if that's not found.
|
||||
if strings.HasPrefix(arg, prometheusConfigFlag) {
|
||||
path = strings.TrimPrefix(arg, prometheusConfigFlag)
|
||||
}
|
||||
if strings.HasPrefix(arg, configReloaderConfigFlag) {
|
||||
path = strings.TrimPrefix(arg, configReloaderConfigFlag)
|
||||
}
|
||||
}
|
||||
if container.Name == configReloaderContainerName {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
if path == "" {
|
||||
err = fmt.Errorf("prometheus config path not found in pod: %s", pod.Name)
|
||||
}
|
||||
return path, err
|
||||
}
|
||||
|
||||
func findPrometheusConfigVolumeAndKey(ctx context.Context, client kubernetes.Interface, pod *corev1.Pod) (*corev1.Volume, string, error) {
|
||||
path, err := findPrometheusConfigPath(ctx, client, pod)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Find the volumeMount the config path is pointing to.
|
||||
var volumeName = ""
|
||||
for _, container := range pod.Spec.Containers {
|
||||
for _, vm := range container.VolumeMounts {
|
||||
if strings.HasPrefix(path, vm.MountPath) {
|
||||
volumeName = vm.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the actual Volume from the name.
|
||||
for _, volume := range pod.Spec.Volumes {
|
||||
if volume.Name == volumeName {
|
||||
return &volume, filepath.Base(path), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, "", errors.New("volume for Prometheus config not found")
|
||||
}
|
||||
|
||||
func extractPrometheusConfigFromVolume(ctx context.Context, client kubernetes.Interface, volume *corev1.Volume, namespace, key string) ([]byte, error) {
|
||||
var b []byte
|
||||
var ok bool
|
||||
// Check for Secret volume.
|
||||
if vs := volume.Secret; vs != nil {
|
||||
s, err := client.CoreV1().Secrets(namespace).Get(ctx, vs.SecretName, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, ok = s.Data[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find file key in secret: %s", key)
|
||||
}
|
||||
}
|
||||
// Check for ConfigMap volume.
|
||||
if vcm := volume.ConfigMap; vcm != nil {
|
||||
cm, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, vcm.Name, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s, ok := cm.Data[key]
|
||||
b = []byte(s)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find file key in configmap: %s", key)
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func unmarshalPromConfigBytes(b []byte) (*promconfig.Config, error) {
|
||||
var config promconfig.Config
|
||||
// Unmarshal the data into a Prometheus config.
|
||||
if err := yaml.Unmarshal(b, &config); err == nil {
|
||||
return &config, nil
|
||||
// If there were errors, try gunziping the data.
|
||||
} else if content := http.DetectContentType(b); content == "application/x-gzip" {
|
||||
r, err := gzip.NewReader(bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return &config, err
|
||||
}
|
||||
gunzipBytes, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return &config, err
|
||||
}
|
||||
err = yaml.Unmarshal(gunzipBytes, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
} else {
|
||||
return &config, err
|
||||
}
|
||||
}
|
||||
105
pkg/integration/prometheus/prometheus.go
Normal file
105
pkg/integration/prometheus/prometheus.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigValidate = "PrometheusConfigValidate"
|
||||
ConfigRelabel = "PrometheusConfigRelabelReport"
|
||||
)
|
||||
|
||||
type Prometheus struct {
|
||||
}
|
||||
|
||||
func NewPrometheus() *Prometheus {
|
||||
return &Prometheus{}
|
||||
}
|
||||
|
||||
func (p *Prometheus) Deploy(namespace string) error {
|
||||
// no-op
|
||||
color.Green("Activating prometheus integration...")
|
||||
// TODO(pintohutch): add timeout or inherit an upstream context
|
||||
// for better signal management.
|
||||
ctx := context.Background()
|
||||
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)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// We just care about existing deployments.
|
||||
// Try and find Prometheus configurations in the cluster using the provided namespace.
|
||||
//
|
||||
// Note: We could cache this state and inject it into the various analyzers
|
||||
// to save additional parsing later.
|
||||
// However, the state of the cluster can change from activation to analysis,
|
||||
// so we would want to run this again on each analyze call anyway.
|
||||
//
|
||||
// One consequence of this is one can run `activate` in one namespace
|
||||
// and run `analyze` in another, without issues, as long as Prometheus
|
||||
// is found in both.
|
||||
// We accept this as a trade-off for the time-being to avoid having the tool
|
||||
// manage Prometheus on the behalf of users.
|
||||
podConfigs, err := findPrometheusPodConfigs(ctx, client.GetClient(), namespace)
|
||||
if err != nil {
|
||||
color.Red("Error discovering Prometheus worklads: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(podConfigs) == 0 {
|
||||
color.Yellow(fmt.Sprintf(`Prometheus installation not found in namespace: %s.
|
||||
Please ensure Prometheus is deployed to analyze.`, namespace))
|
||||
return errors.New("no prometheus installation found")
|
||||
}
|
||||
// Prime state of the analyzer so
|
||||
color.Green("Found existing installation")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Prometheus) UnDeploy(_ string) error {
|
||||
// no-op
|
||||
// We just care about existing deployments.
|
||||
color.Yellow("Integration will leave Prometheus resources deployed. This is an effective no-op in the cluster.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Prometheus) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) {
|
||||
(*mergedMap)[ConfigValidate] = &ConfigAnalyzer{}
|
||||
(*mergedMap)[ConfigRelabel] = &RelabelAnalyzer{}
|
||||
}
|
||||
|
||||
func (p *Prometheus) GetAnalyzerName() []string {
|
||||
return []string{ConfigValidate, ConfigRelabel}
|
||||
}
|
||||
|
||||
func (p *Prometheus) GetNamespace() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *Prometheus) OwnsAnalyzer(analyzer string) bool {
|
||||
return (analyzer == ConfigValidate) || (analyzer == ConfigRelabel)
|
||||
}
|
||||
|
||||
func (t *Prometheus) IsActivate() bool {
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
for _, filter := range t.GetAnalyzerName() {
|
||||
for _, af := range activeFilters {
|
||||
if af == filter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
85
pkg/integration/prometheus/relabel_analyzer.go
Normal file
85
pkg/integration/prometheus/relabel_analyzer.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
discoverykube "github.com/prometheus/prometheus/discovery/kubernetes"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type RelabelAnalyzer struct {
|
||||
}
|
||||
|
||||
func (r *RelabelAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
ctx := a.Context
|
||||
client := a.Client.GetClient()
|
||||
namespace := a.Namespace
|
||||
kind := ConfigRelabel
|
||||
|
||||
podConfigs, err := findPrometheusPodConfigs(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
for _, pc := range podConfigs {
|
||||
var failures []common.Failure
|
||||
pod := pc.pod
|
||||
|
||||
// Check upstream validation.
|
||||
// The Prometheus configuration structs do not generally have validation
|
||||
// methods and embed their validation logic in the UnmarshalYAML methods.
|
||||
config, _ := unmarshalPromConfigBytes(pc.b)
|
||||
// Limit output for brevity.
|
||||
limit := 6
|
||||
i := 0
|
||||
for _, sc := range config.ScrapeConfigs {
|
||||
if i == limit {
|
||||
break
|
||||
}
|
||||
if sc == nil {
|
||||
continue
|
||||
}
|
||||
brc, _ := yaml.Marshal(sc.RelabelConfigs)
|
||||
var bsd []byte
|
||||
for _, cfg := range sc.ServiceDiscoveryConfigs {
|
||||
ks, ok := cfg.(*discoverykube.SDConfig)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
bsd, _ = yaml.Marshal(ks)
|
||||
}
|
||||
// Don't bother with relabel analysis if the scrape config
|
||||
// or service discovery config are empty.
|
||||
if len(brc) == 0 || len(bsd) == 0 {
|
||||
continue
|
||||
}
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("job_name:\n%s\nrelabel_configs:\n%s\nkubernetes_sd_configs:\n%s\n", sc.JobName, string(brc), string(bsd)),
|
||||
})
|
||||
i++
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = common.PreAnalysis{
|
||||
Pod: *pod,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range preAnalysis {
|
||||
var currentAnalysis = common.Result{
|
||||
Kind: kind,
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
parent, _ := util.GetParent(a.Client, value.Pod.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
}
|
||||
@@ -15,33 +15,30 @@ package trivy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type TrivyAnalyzer struct {
|
||||
vulernabilityReportAnalysis bool
|
||||
configAuditReportAnalysis bool
|
||||
}
|
||||
|
||||
func (TrivyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
func (TrivyAnalyzer) analyzeVulnerabilityReports(a common.Analyzer) ([]common.Result, error) {
|
||||
// Get all trivy VulnerabilityReports
|
||||
result := &v1alpha1.VulnerabilityReportList{}
|
||||
|
||||
config := a.Client.GetConfig()
|
||||
// Add group version to sceheme
|
||||
config.ContentConfig.GroupVersion = &v1alpha1.SchemeGroupVersion
|
||||
config.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||
config.APIPath = "/apis"
|
||||
|
||||
restClient, err := rest.UnversionedRESTClientFor(config)
|
||||
client := a.Client.CtrlClient
|
||||
err := v1alpha1.AddToScheme(client.Scheme())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = restClient.Get().Resource("vulnerabilityreports").Do(a.Context).Into(result)
|
||||
if err != nil {
|
||||
if err := client.List(a.Context, result, &ctrl.ListOptions{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -63,8 +60,8 @@ func (TrivyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", report.Labels["trivy-operator.resource.namespace"],
|
||||
report.Labels["trivy-operator.resource.name"])] = common.PreAnalysis{
|
||||
preAnalysis[fmt.Sprintf("%s/%s", report.Namespace,
|
||||
report.Name)] = common.PreAnalysis{
|
||||
TrivyVulnerabilityReport: report,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
@@ -84,4 +81,90 @@ func (TrivyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
|
||||
}
|
||||
|
||||
func (t TrivyAnalyzer) analyzeConfigAuditReports(a common.Analyzer) ([]common.Result, error) {
|
||||
// Get all trivy ConfigAuditReports
|
||||
result := &v1alpha1.ConfigAuditReportList{}
|
||||
|
||||
client := a.Client.CtrlClient
|
||||
err := v1alpha1.AddToScheme(client.Scheme())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.List(a.Context, result, &ctrl.ListOptions{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find criticals and get CVE
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
for _, report := range result.Items {
|
||||
|
||||
// For each k8s resources there may be multiple checks
|
||||
var failures []common.Failure
|
||||
for _, check := range report.Report.Checks {
|
||||
if check.Severity == "MEDIUM" || check.Severity == "HIGH" || check.Severity == "CRITICAL" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Config issue with severity \"%s\" found: %s", check.Severity, strings.Join(check.Messages, "")),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: report.Labels["trivy-operator.resource.name"],
|
||||
Masked: util.MaskString(report.Labels["trivy-operator.resource.name"]),
|
||||
},
|
||||
{
|
||||
Unmasked: report.Labels["trivy-operator.resource.namespace"],
|
||||
Masked: util.MaskString(report.Labels["trivy-operator.resource.namespace"]),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", report.Namespace,
|
||||
report.Name)] = common.PreAnalysis{
|
||||
TrivyConfigAuditReport: report,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, value := range preAnalysis {
|
||||
var currentAnalysis = common.Result{
|
||||
Kind: "ConfigAuditReport",
|
||||
Name: key,
|
||||
Error: value.FailureDetails,
|
||||
}
|
||||
|
||||
parent, _ := util.GetParent(a.Client, value.TrivyConfigAuditReport.ObjectMeta)
|
||||
currentAnalysis.ParentObject = parent
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
}
|
||||
|
||||
func (t TrivyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
if t.vulernabilityReportAnalysis {
|
||||
common := make([]common.Result, 0)
|
||||
vresult, err := t.analyzeVulnerabilityReports(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
common = append(common, vresult...)
|
||||
return common, nil
|
||||
}
|
||||
if t.configAuditReportAnalysis {
|
||||
common := make([]common.Result, 0)
|
||||
cresult, err := t.analyzeConfigAuditReports(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
common = append(common, cresult...)
|
||||
return common, nil
|
||||
}
|
||||
return make([]common.Result, 0), nil
|
||||
}
|
||||
|
||||
@@ -16,24 +16,39 @@ package trivy
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
helmclient "github.com/mittwald/go-helm-client"
|
||||
"github.com/spf13/viper"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
)
|
||||
|
||||
const (
|
||||
Repo = "https://aquasecurity.github.io/helm-charts/"
|
||||
Version = "0.13.0"
|
||||
ChartName = "trivy-operator"
|
||||
RepoShortName = "aqua"
|
||||
ReleaseName = "trivy-operator-k8sgpt"
|
||||
var (
|
||||
Repo = getEnv("TRIVY_REPO", "https://aquasecurity.github.io/helm-charts/")
|
||||
Version = getEnv("TRIVY_VERSION", "0.13.0")
|
||||
ChartName = getEnv("TRIVY_CHART_NAME", "trivy-operator")
|
||||
RepoShortName = getEnv("TRIVY_REPO_SHORT_NAME", "aqua")
|
||||
ReleaseName = getEnv("TRIVY_RELEASE_NAME", "trivy-operator-k8sgpt")
|
||||
)
|
||||
|
||||
type Trivy struct {
|
||||
helm helmclient.Client
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func NewTrivy() *Trivy {
|
||||
helmClient, err := helmclient.New(&helmclient.Options{})
|
||||
if err != nil {
|
||||
@@ -44,10 +59,36 @@ func NewTrivy() *Trivy {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Trivy) GetAnalyzerName() string {
|
||||
return "VulnerabilityReport"
|
||||
func (t *Trivy) GetAnalyzerName() []string {
|
||||
return []string{
|
||||
"VulnerabilityReport",
|
||||
"ConfigAuditReport",
|
||||
}
|
||||
}
|
||||
|
||||
// This doesnt work
|
||||
func (t *Trivy) GetNamespace() (string, error) {
|
||||
releases, err := t.helm.ListDeployedReleases()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, rel := range releases {
|
||||
if rel.Name == ReleaseName {
|
||||
return rel.Namespace, nil
|
||||
}
|
||||
}
|
||||
return "", status.Error(codes.NotFound, "trivy release not found")
|
||||
}
|
||||
|
||||
func (t *Trivy) OwnsAnalyzer(analyzer string) bool {
|
||||
|
||||
for _, a := range t.GetAnalyzerName() {
|
||||
if analyzer == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (t *Trivy) Deploy(namespace string) error {
|
||||
|
||||
// Add the repository
|
||||
@@ -55,7 +96,6 @@ func (t *Trivy) Deploy(namespace string) error {
|
||||
Name: RepoShortName,
|
||||
URL: Repo,
|
||||
}
|
||||
|
||||
// Add a chart-repository to the client.
|
||||
if err := t.helm.AddOrUpdateChartRepo(chartRepo); err != nil {
|
||||
panic(err)
|
||||
@@ -65,9 +105,12 @@ func (t *Trivy) Deploy(namespace string) error {
|
||||
ReleaseName: ReleaseName,
|
||||
ChartName: fmt.Sprintf("%s/%s", RepoShortName, ChartName),
|
||||
Namespace: namespace,
|
||||
UpgradeCRDs: true,
|
||||
Wait: false,
|
||||
Timeout: 300,
|
||||
|
||||
//TODO: All of this should be configurable
|
||||
UpgradeCRDs: true,
|
||||
Wait: false,
|
||||
Timeout: 300,
|
||||
CreateNamespace: true,
|
||||
}
|
||||
|
||||
// Install a chart release.
|
||||
@@ -96,21 +139,61 @@ func (t *Trivy) UnDeploy(namespace string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Trivy) IsActivate() bool {
|
||||
|
||||
if _, err := t.helm.GetRelease(ReleaseName); err != nil {
|
||||
return false
|
||||
func (t *Trivy) isDeployed() bool {
|
||||
// check if aquasec apigroup is available as a marker if trivy is installed on the cluster
|
||||
kubecontext := viper.GetString("kubecontext")
|
||||
kubeconfig := viper.GetString("kubeconfig")
|
||||
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
|
||||
if err != nil {
|
||||
// TODO: better error handling
|
||||
color.Red("Error initialising kubernetes client: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
groups, _, err := client.Client.Discovery().ServerGroupsAndResources()
|
||||
if err != nil {
|
||||
// TODO: better error handling
|
||||
color.Red("Error initialising discovery client: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return true
|
||||
for _, group := range groups {
|
||||
if group.Name == "aquasecurity.github.io" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Trivy) isFilterActive() bool {
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
for _, filter := range t.GetAnalyzerName() {
|
||||
for _, af := range activeFilters {
|
||||
if af == filter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Trivy) IsActivate() bool {
|
||||
if t.isFilterActive() && t.isDeployed() {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Trivy) AddAnalyzer(mergedMap *map[string]common.IAnalyzer) {
|
||||
|
||||
(*mergedMap)["VulnerabilityReport"] = &TrivyAnalyzer{}
|
||||
(*mergedMap)["VulnerabilityReport"] = &TrivyAnalyzer{
|
||||
vulernabilityReportAnalysis: true,
|
||||
}
|
||||
(*mergedMap)["ConfigAuditReport"] = &TrivyAnalyzer{
|
||||
configAuditReportAnalysis: true,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *Trivy) RemoveAnalyzer() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
106
pkg/kubernetes/apireference_test.go
Normal file
106
pkg/kubernetes/apireference_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
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 kubernetes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
openapi_v2 "github.com/google/gnostic/openapiv2"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func TestGetApiDocV2(t *testing.T) {
|
||||
k8s := &K8sApiReference{
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "group.v1",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: &openapi_v2.Document{
|
||||
Definitions: &openapi_v2.Definitions{
|
||||
AdditionalProperties: []*openapi_v2.NamedSchema{
|
||||
{
|
||||
Name: "group.v1.kind",
|
||||
Value: &openapi_v2.Schema{
|
||||
Title: "test",
|
||||
Properties: &openapi_v2.Properties{
|
||||
AdditionalProperties: []*openapi_v2.NamedSchema{
|
||||
{
|
||||
Name: "schema1",
|
||||
Value: &openapi_v2.Schema{
|
||||
Title: "test",
|
||||
Description: "schema1 description",
|
||||
Type: &openapi_v2.TypeItem{
|
||||
Value: []string{"string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "schema2",
|
||||
Value: &openapi_v2.Schema{
|
||||
Items: &openapi_v2.ItemsItem{
|
||||
Schema: []*openapi_v2.Schema{
|
||||
{
|
||||
Title: "random-schema",
|
||||
},
|
||||
},
|
||||
},
|
||||
Title: "test",
|
||||
XRef: "xref",
|
||||
Description: "schema2 description",
|
||||
Type: &openapi_v2.TypeItem{
|
||||
Value: []string{"bool"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "group",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Kind: "kind",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "empty field",
|
||||
},
|
||||
{
|
||||
name: "2 schemas",
|
||||
field: "schema2.schema1",
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
name: "schema1 description",
|
||||
field: "schema1",
|
||||
expectedOutput: "schema1 description",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output := k8s.GetApiDocV2(tt.field)
|
||||
require.Equal(t, tt.expectedOutput, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user