mirror of
https://github.com/k8sgpt-ai/k8sgpt.git
synced 2026-03-18 19:17:25 +00:00
Compare commits
195 Commits
v0.3.45
...
renovate/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c51a0c3f6 | ||
|
|
2276b12b0f | ||
|
|
fd5bba6ab3 | ||
|
|
19a172e575 | ||
|
|
36de157e21 | ||
|
|
458aa9deba | ||
|
|
285c1353d5 | ||
|
|
4f63e9737c | ||
|
|
a56e4788c3 | ||
|
|
99911fbb3a | ||
|
|
abc46474e3 | ||
|
|
1a8f1d47a4 | ||
|
|
1f2ff98834 | ||
|
|
c80b2e2c34 | ||
|
|
867bce1907 | ||
|
|
f5fb2a7e12 | ||
|
|
a303ffa21c | ||
|
|
21369c5c09 | ||
|
|
40ffcbec6b | ||
|
|
7fe3bdbd95 | ||
|
|
e7b7a5db47 | ||
|
|
5480051230 | ||
|
|
ee6f58443b | ||
|
|
f1d2e306f3 | ||
|
|
83c5d67084 | ||
|
|
291e42dc4b | ||
|
|
8bbffed643 | ||
|
|
53345895de | ||
|
|
7e332761d8 | ||
|
|
0239b2fe6e | ||
|
|
c5c9135900 | ||
|
|
5e86f4925c | ||
|
|
0cf4cae07e | ||
|
|
e385e77da9 | ||
|
|
c47ae595fb | ||
|
|
00c99dc934 | ||
|
|
a821814125 | ||
|
|
50d5d78c06 | ||
|
|
5b4224951e | ||
|
|
290a4be210 | ||
|
|
fe4608793d | ||
|
|
3a1187ad5a | ||
|
|
1819e6f410 | ||
|
|
392c79d0be | ||
|
|
00c07999e2 | ||
|
|
8002d94345 | ||
|
|
0c917fc601 | ||
|
|
08f2855a4d | ||
|
|
57d32720dc | ||
|
|
0f700f0cd3 | ||
|
|
74fbde0053 | ||
|
|
ff619481cc | ||
|
|
5636515db9 | ||
|
|
47f09ac686 | ||
|
|
be4fb1cc03 | ||
|
|
5947876e49 | ||
|
|
7d4cb26713 | ||
|
|
6b9f346bf6 | ||
|
|
42654e7f55 | ||
|
|
7dfe8bef0f | ||
|
|
dfcc5dc5a1 | ||
|
|
d7cb19ad29 | ||
|
|
306b3c9997 | ||
|
|
1e57b7774c | ||
|
|
d308c511fb | ||
|
|
4faf77d91a | ||
|
|
b2241c03c9 | ||
|
|
0b7ddf5e3b | ||
|
|
d0f03641ae | ||
|
|
e76bdb0c23 | ||
|
|
cae94e7b6d | ||
|
|
7e375a30be | ||
|
|
34ff645fa0 | ||
|
|
61b60d5768 | ||
|
|
6a81d2c140 | ||
|
|
21bc76e5b7 | ||
|
|
d5341f3c00 | ||
|
|
752a16c407 | ||
|
|
81da402d46 | ||
|
|
f2f25edef7 | ||
|
|
85935a46d8 | ||
|
|
a56e663169 | ||
|
|
e41ffd80d0 | ||
|
|
f603948935 | ||
|
|
67f5855695 | ||
|
|
ebb0373f69 | ||
|
|
3b6ad06de1 | ||
|
|
443469960a | ||
|
|
17863c24d5 | ||
|
|
e588fc316d | ||
|
|
a128906136 | ||
|
|
0553b984b7 | ||
|
|
96d86d3eb0 | ||
|
|
df17e3e728 | ||
|
|
80904e3063 | ||
|
|
cf6f9289e1 | ||
|
|
a79224e2bf | ||
|
|
9ce33469d8 | ||
|
|
969fe99b33 | ||
|
|
91d423b147 | ||
|
|
766b51cd3e | ||
|
|
060a3b2a26 | ||
|
|
ce4b3c2e7d | ||
|
|
78ffa5904a | ||
|
|
dceda9a6a1 | ||
|
|
e7783482ce | ||
|
|
a5574ee49d | ||
|
|
f68ff0efee | ||
|
|
0c63044254 | ||
|
|
39ae2aa635 | ||
|
|
05040da188 | ||
|
|
9bffc7cff7 | ||
|
|
c5fe2c68d1 | ||
|
|
c1b267b818 | ||
|
|
5086ccd659 | ||
|
|
b6261026f8 | ||
|
|
1681aadac1 | ||
|
|
9874cef8bf | ||
|
|
3dbc9e1a20 | ||
|
|
e0d66f43f7 | ||
|
|
d4de5d9e3f | ||
|
|
5b7fb7e619 | ||
|
|
68ddac0089 | ||
|
|
d1a29e4001 | ||
|
|
ad2c90a129 | ||
|
|
e4861e9e2d | ||
|
|
3c353b0e93 | ||
|
|
c823de12e6 | ||
|
|
e231032e1b | ||
|
|
f0b18cfb1c | ||
|
|
a31d07c802 | ||
|
|
06d201ca5d | ||
|
|
a962741220 | ||
|
|
a75ec50789 | ||
|
|
e5817f9e55 | ||
|
|
f5eaf817f0 | ||
|
|
eb381b8087 | ||
|
|
288ca862b3 | ||
|
|
81d4aaf402 | ||
|
|
fdf8e7a95a | ||
|
|
5a48bae667 | ||
|
|
7540e0084e | ||
|
|
eb7b36aa27 | ||
|
|
d6d2e3bc42 | ||
|
|
4e39cb65b3 | ||
|
|
db5e517dbb | ||
|
|
aa1e237ebb | ||
|
|
f2fdfd8dca | ||
|
|
e14c3dad55 | ||
|
|
093975e50d | ||
|
|
4f4f4f13a0 | ||
|
|
2a6f48500c | ||
|
|
f2e3b9a8a7 | ||
|
|
d1b2227ff9 | ||
|
|
1f953585c9 | ||
|
|
9dcb21e160 | ||
|
|
d956f32e1e | ||
|
|
7dadea2570 | ||
|
|
3b85f09348 | ||
|
|
06b8f78150 | ||
|
|
076ca2f148 | ||
|
|
fcc8563e4e | ||
|
|
5de4f7704a | ||
|
|
83672fa768 | ||
|
|
990d723909 | ||
|
|
c506a4b441 | ||
|
|
2918556793 | ||
|
|
19abbef9a3 | ||
|
|
939e0672aa | ||
|
|
8cd3b2985e | ||
|
|
1827c43696 | ||
|
|
1363219b1b | ||
|
|
c3f448693d | ||
|
|
5514ebb53b | ||
|
|
c21ba86237 | ||
|
|
d6d80ee860 | ||
|
|
a841568a9c | ||
|
|
4d7eb0f622 | ||
|
|
a12aa07b1a | ||
|
|
ec5e42b8f4 | ||
|
|
69c67bd1d9 | ||
|
|
b3f60b2d20 | ||
|
|
cb1e1ffede | ||
|
|
f37d923918 | ||
|
|
a50375c960 | ||
|
|
da266b3c82 | ||
|
|
896a53be83 | ||
|
|
2da057360b | ||
|
|
69fd7c7696 | ||
|
|
ad86e7aa39 | ||
|
|
1ae70e806e | ||
|
|
2ce8450e03 | ||
|
|
b6b3d0c856 | ||
|
|
5f7d9de46a | ||
|
|
7dcdfc83d2 |
105
.github/workflows/build_container.yaml
vendored
105
.github/workflows/build_container.yaml
vendored
@@ -8,13 +8,16 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- fix/build-branch
|
||||
- '[0-9]+.[1-9][0-9]*.x'
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
|
||||
env:
|
||||
GO_VERSION: "~1.22"
|
||||
GO_VERSION: "~1.24"
|
||||
IMAGE_NAME: "k8sgpt"
|
||||
REGISTRY_IMAGE: ghcr.io/k8sgpt-ai/k8sgpt
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -22,7 +25,7 @@ defaults:
|
||||
jobs:
|
||||
prepare_ci_run:
|
||||
name: Prepare CI Run
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
GIT_SHA: ${{ steps.extract_branch.outputs.GIT_SHA }}
|
||||
BRANCH: ${{ steps.extract_branch.outputs.BRANCH }}
|
||||
@@ -33,7 +36,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
|
||||
- name: Extract branch name
|
||||
id: extract_branch
|
||||
@@ -51,97 +54,61 @@ jobs:
|
||||
id: get_run_type
|
||||
run: |
|
||||
NON_FORKED_AND_NON_ROBOT_RUN=${{ ( github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]' ) && ( github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository ) }}
|
||||
echo "github.actor != 'renovate[bot]' = ${{ github.actor != 'renovate[bot]' }}"
|
||||
echo "github.actor != 'dependabot[bot]' = ${{ github.actor != 'dependabot[bot]' }}"
|
||||
echo "github.event_name == 'push' = ${{ github.event_name == 'push' }}"
|
||||
echo "github.event.pull_request.head.repo.full_name == github.repository = ${{ github.event.pull_request.head.repo.full_name == github.repository }}"
|
||||
echo "NON_FORKED_AND_NON_ROBOT_RUN = $NON_FORKED_AND_NON_ROBOT_RUN"
|
||||
echo "NON_FORKED_AND_NON_ROBOT_RUN=$NON_FORKED_AND_NON_ROBOT_RUN" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build_image:
|
||||
name: Build Container Image
|
||||
build-and-push:
|
||||
name: Build and Push Multi-arch Image
|
||||
needs: prepare_ci_run
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.prepare_ci_run.outputs.NON_FORKED_AND_NON_ROBOT_RUN == 'true' }}
|
||||
env:
|
||||
BRANCH: ${{ needs.prepare_ci_run.outputs.BRANCH }}
|
||||
DATETIME: ${{ needs.prepare_ci_run.outputs.DATETIME }}
|
||||
BUILD_TIME: ${{ needs.prepare_ci_run.outputs.BUILD_TIME }}
|
||||
GIT_SHA: ${{ needs.prepare_ci_run.outputs.GIT_SHA }}
|
||||
RELEASE_REGISTRY: "localhost:5000/k8sgpt"
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
file: ./container/Dockerfile
|
||||
target: production
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
${{ env.RELEASE_REGISTRY }}/${{ env.IMAGE_NAME }}:dev-${{ env.DATETIME }}
|
||||
build-args: |
|
||||
GIT_HASH=${{ env.GIT_SHA }}
|
||||
RELEASE_VERSION=dev-${{ env.DATETIME }}
|
||||
BUILD_TIME=${{ env.BUILD_TIME }}
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: false
|
||||
cache-from: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_NAME }}
|
||||
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_NAME }}
|
||||
outputs: type=docker,dest=/tmp/${{ env.IMAGE_NAME }}-image.tar
|
||||
|
||||
- name: Upload image as artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
|
||||
with:
|
||||
name: ${{ env.IMAGE_NAME }}-image.tar
|
||||
path: /tmp/${{ env.IMAGE_NAME }}-image.tar
|
||||
|
||||
upload_images:
|
||||
name: Upload images to ghcr registry
|
||||
needs: [ prepare_ci_run, build_image ]
|
||||
if: github.event_name == 'push' && needs.prepare_ci_run.outputs.NON_FORKED_AND_NON_ROBOT_RUN == 'true' # only run on push to main/maintenance branches
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
DATETIME: ${{ needs.prepare_ci_run.outputs.DATETIME }}
|
||||
BUILD_TIME: ${{ needs.prepare_ci_run.outputs.BUILD_TIME }}
|
||||
GIT_SHA: ${{ needs.prepare_ci_run.outputs.GIT_SHA }}
|
||||
permissions:
|
||||
packages: write # Needed for pushing images to the registry
|
||||
contents: read # Needed for checking out the repository
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=dev-${{ env.DATETIME }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
password: ${{ secrets.K8SGPT_BOT_SECRET }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
|
||||
- name: Build and push multi-arch image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
context: .
|
||||
file: ./container/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
target: production
|
||||
tags: |
|
||||
ghcr.io/k8sgpt-ai/${{ env.IMAGE_NAME }}:dev-${{ env.DATETIME }}
|
||||
build-args: |
|
||||
GIT_HASH=${{ env.GIT_SHA }}
|
||||
RELEASE_VERSION=dev-${{ env.DATETIME }}
|
||||
BUILD_TIME=${{ env.BUILD_TIME }}
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: true
|
||||
cache-from: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_NAME }}
|
||||
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
${{ env.REGISTRY_IMAGE }}:${{ env.DATETIME }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
secrets: |
|
||||
GIT_AUTH_TOKEN=${{ secrets.K8SGPT_BOT_SECRET }}
|
||||
|
||||
10
.github/workflows/golangci_lint.yaml
vendored
10
.github/workflows/golangci_lint.yaml
vendored
@@ -9,12 +9,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
|
||||
- name: golangci-lint
|
||||
uses: reviewdog/action-golangci-lint@7708105983c614f7a2725e2172908b7709d1c3e4 # v2
|
||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-pr-check
|
||||
golangci_lint_flags: "--timeout=240s"
|
||||
level: warning
|
||||
version: v2.1.0
|
||||
only-new-issues: true
|
||||
31
.github/workflows/release.yaml
vendored
31
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
# Release-please creates a PR that tracks all changes
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
|
||||
- uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1
|
||||
id: release
|
||||
@@ -55,17 +55,17 @@ jobs:
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '~1.24'
|
||||
- name: Download Syft
|
||||
uses: anchore/sbom-action/download-syft@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7
|
||||
uses: anchore/sbom-action/download-syft@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6
|
||||
with:
|
||||
# either 'goreleaser' (default) or 'goreleaser-pro'
|
||||
distribution: goreleaser
|
||||
@@ -73,8 +73,9 @@ jobs:
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.K8SGPT_BOT_SECRET }}
|
||||
- name: Update new version in krew-index
|
||||
uses: rajatjindal/krew-release-bot@df3eb197549e3568be8b4767eec31c5e8e8e6ad8 # v0.0.46
|
||||
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
|
||||
# - name: Update new version in krew-index
|
||||
# uses: rajatjindal/krew-release-bot@3d9faef30a82761d610544f62afddca00993eef9 # v0.0.47
|
||||
|
||||
build-container:
|
||||
if: needs.release-please.outputs.releases_created == 'true'
|
||||
@@ -90,23 +91,23 @@ jobs:
|
||||
IMAGE_NAME: k8sgpt
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
password: ${{ secrets.K8SGPT_BOT_SECRET }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
context: .
|
||||
file: ./container/Dockerfile
|
||||
@@ -120,14 +121,14 @@ jobs:
|
||||
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_TAG }}
|
||||
|
||||
- name: Generate SBOM
|
||||
uses: anchore/sbom-action@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7
|
||||
uses: anchore/sbom-action@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8
|
||||
with:
|
||||
image: ${{ env.IMAGE_TAG }}
|
||||
artifact-name: sbom-${{ env.IMAGE_NAME }}
|
||||
output-file: ./sbom-${{ env.IMAGE_NAME }}.spdx.json
|
||||
|
||||
- name: Attach SBOM to release
|
||||
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2
|
||||
with:
|
||||
tag_name: ${{ needs.release-please.outputs.tag_name }}
|
||||
files: ./sbom-${{ env.IMAGE_NAME }}.spdx.json
|
||||
|
||||
4
.github/workflows/semantic_pr.yaml
vendored
4
.github/workflows/semantic_pr.yaml
vendored
@@ -10,13 +10,13 @@ defaults:
|
||||
shell: bash
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # Needed for checking out the repository
|
||||
pull-requests: read # Needed for reading prs
|
||||
steps:
|
||||
- name: Validate Pull Request
|
||||
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
|
||||
uses: amannn/action-semantic-pull-request@fdd4d3ddf614fbcd8c29e4b106d3bbe0cb2c605d # v6.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
8
.github/workflows/test.yaml
vendored
8
.github/workflows/test.yaml
vendored
@@ -9,22 +9,22 @@ on:
|
||||
- main
|
||||
|
||||
env:
|
||||
GO_VERSION: "~1.22"
|
||||
GO_VERSION: "~1.24"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5
|
||||
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Run test
|
||||
run: go test ./... -coverprofile=coverage.txt
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ k8sgpt*
|
||||
dist/
|
||||
|
||||
bin/
|
||||
pkg/server/example/example
|
||||
@@ -70,8 +70,28 @@ checksum:
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
|
||||
# skip: true
|
||||
# The lines beneath this are called `modelines`. See `:help modeline`
|
||||
# Feel free to remove those if you don't want/use them.
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
announce:
|
||||
slack:
|
||||
# Whether its enabled or not.
|
||||
#
|
||||
# Templates: allowed (since v2.6).
|
||||
enabled: true
|
||||
|
||||
# Message template to use while publishing.
|
||||
#
|
||||
# Default: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'.
|
||||
# Templates: allowed.
|
||||
message_template: "{{ .ProjectName }} release {{.Tag}} is out!"
|
||||
|
||||
# The name of the channel that the user selected as a destination for webhook messages.
|
||||
channel: "#general"
|
||||
|
||||
# Set your Webhook's user name.
|
||||
username: "K8sGPT"
|
||||
|
||||
# Emoji to use as the icon for this message. Overrides icon_url.
|
||||
icon_emoji: ""
|
||||
|
||||
# URL to an image to use as the icon for this message.
|
||||
icon_url: ""
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{".":"0.3.45"}
|
||||
{".":"0.4.30"}
|
||||
514
CHANGELOG.md
514
CHANGELOG.md
@@ -1,5 +1,519 @@
|
||||
# Changelog
|
||||
|
||||
## [0.4.30](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.29...v0.4.30) (2026-02-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* validate namespace before running custom analyzers ([#1617](https://github.com/k8sgpt-ai/k8sgpt/issues/1617)) ([458aa9d](https://github.com/k8sgpt-ai/k8sgpt/commit/458aa9debac7590eb0855ffd12141b702e999a36))
|
||||
|
||||
## [0.4.29](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.28...v0.4.29) (2026-02-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **serve:** add short flag and env var for metrics port ([#1616](https://github.com/k8sgpt-ai/k8sgpt/issues/1616)) ([4f63e97](https://github.com/k8sgpt-ai/k8sgpt/commit/4f63e9737c6a2306686bd3b6f37e81f210665949))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to b8788ab ([#1572](https://github.com/k8sgpt-ai/k8sgpt/issues/1572)) ([a56e478](https://github.com/k8sgpt-ai/k8sgpt/commit/a56e4788c3361a64df17175f163f33422a8fe606))
|
||||
* use proper JSON marshaling for customrest prompt to handle special characters ([#1615](https://github.com/k8sgpt-ai/k8sgpt/issues/1615)) ([99911fb](https://github.com/k8sgpt-ai/k8sgpt/commit/99911fbb3ac8c950fd7ee1b3210f8a9c2a6b0ad7)), closes [#1556](https://github.com/k8sgpt-ai/k8sgpt/issues/1556)
|
||||
|
||||
|
||||
### Refactoring
|
||||
|
||||
* improve MCP server handlers with better error handling and pagination ([#1613](https://github.com/k8sgpt-ai/k8sgpt/issues/1613)) ([abc4647](https://github.com/k8sgpt-ai/k8sgpt/commit/abc46474e372bcd27201f1a64372c04269acee13))
|
||||
|
||||
## [0.4.28](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.27...v0.4.28) (2026-02-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Groq as LLM provider ([#1600](https://github.com/k8sgpt-ai/k8sgpt/issues/1600)) ([867bce1](https://github.com/k8sgpt-ai/k8sgpt/commit/867bce1907f5dd3387128b72c694e98091d55554))
|
||||
* multiple security fixes. Prometheus: v0.302.1 → v0.306.0 ([#1597](https://github.com/k8sgpt-ai/k8sgpt/issues/1597)) ([f5fb2a7](https://github.com/k8sgpt-ai/k8sgpt/commit/f5fb2a7e12e14fad8107940aeead5e60b064add1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* align CI Go versions with go.mod to ensure consistency ([#1611](https://github.com/k8sgpt-ai/k8sgpt/issues/1611)) ([1f2ff98](https://github.com/k8sgpt-ai/k8sgpt/commit/1f2ff988342b8ef2aa3e3263eb845c0ee09fe24c))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1550](https://github.com/k8sgpt-ai/k8sgpt/issues/1550)) ([7fe3bdb](https://github.com/k8sgpt-ai/k8sgpt/commit/7fe3bdbd952bc9a1975121de5f21ad31dc1f691d))
|
||||
* use MaxCompletionTokens instead of deprecated MaxTokens for OpenAI ([#1604](https://github.com/k8sgpt-ai/k8sgpt/issues/1604)) ([c80b2e2](https://github.com/k8sgpt-ai/k8sgpt/commit/c80b2e2c346845336593ce515fe90fd501b1d0a7))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/checkout digest to 93cb6ef ([#1592](https://github.com/k8sgpt-ai/k8sgpt/issues/1592)) ([40ffcbe](https://github.com/k8sgpt-ai/k8sgpt/commit/40ffcbec6b65e3a99e40be5f414a3f2c087bffbb))
|
||||
* **deps:** update actions/setup-go digest to 40f1582 ([#1593](https://github.com/k8sgpt-ai/k8sgpt/issues/1593)) ([a303ffa](https://github.com/k8sgpt-ai/k8sgpt/commit/a303ffa21c7ede3dd9391185bc91fb3b4e8276b6))
|
||||
* util tests ([#1594](https://github.com/k8sgpt-ai/k8sgpt/issues/1594)) ([21369c5](https://github.com/k8sgpt-ai/k8sgpt/commit/21369c5c0917fd2b6ae4173378b2e257e2b1de7b))
|
||||
|
||||
## [0.4.27](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.26...v0.4.27) (2025-12-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* mcp v2 ([#1589](https://github.com/k8sgpt-ai/k8sgpt/issues/1589)) ([5480051](https://github.com/k8sgpt-ai/k8sgpt/commit/5480051230ce83b89c0382abd7992c7ecc4a85b8))
|
||||
|
||||
## [0.4.26](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.25...v0.4.26) (2025-10-16)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* missing filter arg on serve ([#1583](https://github.com/k8sgpt-ai/k8sgpt/issues/1583)) ([f1d2e30](https://github.com/k8sgpt-ai/k8sgpt/commit/f1d2e306f32eb1e01a2788174084be29a7fa1282))
|
||||
|
||||
## [0.4.25](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.24...v0.4.25) (2025-09-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* fix to broken inference ([#1575](https://github.com/k8sgpt-ai/k8sgpt/issues/1575)) ([291e42d](https://github.com/k8sgpt-ai/k8sgpt/commit/291e42dc4b81ffb0672c21fbb325ddebc5d531a3))
|
||||
|
||||
## [0.4.24](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.23...v0.4.24) (2025-08-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ClusterServiceVersion, Subscription, InstallPlan, OperatorGroup, and CatalogSource analyzers ([#1564](https://github.com/k8sgpt-ai/k8sgpt/issues/1564)) ([0cf4cae](https://github.com/k8sgpt-ai/k8sgpt/commit/0cf4cae07e32a0025246abcf2d1a5a91f82d093a))
|
||||
* reintroduced inference code ([#1548](https://github.com/k8sgpt-ai/k8sgpt/issues/1548)) ([7e33276](https://github.com/k8sgpt-ai/k8sgpt/commit/7e332761d89d953989b4f33509208dd4db4d4b91))
|
||||
* update helm charts with mcp support and fix Google ADA issue ([#1568](https://github.com/k8sgpt-ai/k8sgpt/issues/1568)) ([5334589](https://github.com/k8sgpt-ai/k8sgpt/commit/53345895deec4c74cac00ee3fd5e230f6a92cf4a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* migrated to more actively maintained mcp golang lib and added AI explain ([#1557](https://github.com/k8sgpt-ai/k8sgpt/issues/1557)) ([c47ae59](https://github.com/k8sgpt-ai/k8sgpt/commit/c47ae595fb9fc5bf22afef3bc6764b3e87e4553d))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/checkout action to v5 ([#1562](https://github.com/k8sgpt-ai/k8sgpt/issues/1562)) ([e385e77](https://github.com/k8sgpt-ai/k8sgpt/commit/e385e77da93a65fe52a152bf1f8f1415552698d5))
|
||||
* **deps:** update amannn/action-semantic-pull-request action to v6 ([#1565](https://github.com/k8sgpt-ai/k8sgpt/issues/1565)) ([c5c9135](https://github.com/k8sgpt-ai/k8sgpt/commit/c5c9135900ec6f95b63dac47df751269e7420e87))
|
||||
* **deps:** update docker/login-action digest to 184bdaa ([#1559](https://github.com/k8sgpt-ai/k8sgpt/issues/1559)) ([0239b2f](https://github.com/k8sgpt-ai/k8sgpt/commit/0239b2fe6e7105bbcf3256c559c30ec7065b25f3))
|
||||
* **deps:** update goreleaser/goreleaser-action digest to e435ccd ([#1569](https://github.com/k8sgpt-ai/k8sgpt/issues/1569)) ([5e86f49](https://github.com/k8sgpt-ai/k8sgpt/commit/5e86f4925c4209b0eb2959227229c2994cfc5b6f))
|
||||
|
||||
## [0.4.23](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.22...v0.4.23) (2025-08-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ClusterCatalog and ClusterExtension analyzers ([#1555](https://github.com/k8sgpt-ai/k8sgpt/issues/1555)) ([a821814](https://github.com/k8sgpt-ai/k8sgpt/commit/a821814125e25c062ff2faebf9df1b880414c22c))
|
||||
* oci genai chat models ([#1337](https://github.com/k8sgpt-ai/k8sgpt/issues/1337)) ([290a4be](https://github.com/k8sgpt-ai/k8sgpt/commit/290a4be210fbb508214070c31218138781d96142))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1537](https://github.com/k8sgpt-ai/k8sgpt/issues/1537)) ([50d5d78](https://github.com/k8sgpt-ai/k8sgpt/commit/50d5d78c06e42d75a2448989528e5e6be12ea825))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.17.4 [security] ([#1541](https://github.com/k8sgpt-ai/k8sgpt/issues/1541)) ([5b42249](https://github.com/k8sgpt-ai/k8sgpt/commit/5b4224951e7348e9d78292dadc9b9786957117f1))
|
||||
|
||||
## [0.4.22](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.21...v0.4.22) (2025-07-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add APAC region Claude models support for Amazon Bedrock ([#1543](https://github.com/k8sgpt-ai/k8sgpt/issues/1543)) ([1819e6f](https://github.com/k8sgpt-ai/k8sgpt/commit/1819e6f410d078fce2bda8bbdb22054dfb4fc092))
|
||||
* add streamable-http support for MCP server ([#1546](https://github.com/k8sgpt-ai/k8sgpt/issues/1546)) ([3a1187a](https://github.com/k8sgpt-ai/k8sgpt/commit/3a1187ad5a190713b9216cf6d9d52d54cdb3e4da))
|
||||
|
||||
## [0.4.21](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.20...v0.4.21) (2025-06-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add latest and legacy stable models ([#1539](https://github.com/k8sgpt-ai/k8sgpt/issues/1539)) ([00c0799](https://github.com/k8sgpt-ai/k8sgpt/commit/00c07999e2290e70a6ecb95b255b4924f55ecd5f))
|
||||
* support for claude4 && model names listed ([#1540](https://github.com/k8sgpt-ai/k8sgpt/issues/1540)) ([8002d94](https://github.com/k8sgpt-ai/k8sgpt/commit/8002d943453aac8c3675d7072b25dfdc3aec1c1d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1511](https://github.com/k8sgpt-ai/k8sgpt/issues/1511)) ([08f2855](https://github.com/k8sgpt-ai/k8sgpt/commit/08f2855a4d7e61f3422cb68b0966272a85f617a5))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update docker/setup-buildx-action digest to e468171 ([#1527](https://github.com/k8sgpt-ai/k8sgpt/issues/1527)) ([0c917fc](https://github.com/k8sgpt-ai/k8sgpt/commit/0c917fc60115ef0dc775e858a55964382b20c5e1))
|
||||
|
||||
## [0.4.20](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.19...v0.4.20) (2025-06-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added cache purge ([#1532](https://github.com/k8sgpt-ai/k8sgpt/issues/1532)) ([74fbde0](https://github.com/k8sgpt-ai/k8sgpt/commit/74fbde00537e627c408b317ff9098227be11e2ad))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* model name ([#1535](https://github.com/k8sgpt-ai/k8sgpt/issues/1535)) ([0f700f0](https://github.com/k8sgpt-ai/k8sgpt/commit/0f700f0cd39bf5881d6c05240b842f4df7a6c016))
|
||||
|
||||
## [0.4.19](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.18...v0.4.19) (2025-06-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* fixed haiku ([#1530](https://github.com/k8sgpt-ai/k8sgpt/issues/1530)) ([5636515](https://github.com/k8sgpt-ai/k8sgpt/commit/5636515db98b529689a214af5066d50b5e42d3a1))
|
||||
|
||||
## [0.4.18](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.17...v0.4.18) (2025-06-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to 4c0f3b2 ([#1523](https://github.com/k8sgpt-ai/k8sgpt/issues/1523)) ([7d4cb26](https://github.com/k8sgpt-ai/k8sgpt/commit/7d4cb267130f60088350213482795f37594cb0bc))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1509](https://github.com/k8sgpt-ai/k8sgpt/issues/1509)) ([d7cb19a](https://github.com/k8sgpt-ai/k8sgpt/commit/d7cb19ad29c92eaba552ba723945c937fc3c42da))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update codecov/codecov-action digest to 18283e0 ([#1513](https://github.com/k8sgpt-ai/k8sgpt/issues/1513)) ([42654e7](https://github.com/k8sgpt-ai/k8sgpt/commit/42654e7f55d7a9e9be5b664adaaa8979106e7298))
|
||||
* **deps:** update docker/build-push-action digest to 1dc7386 ([#1512](https://github.com/k8sgpt-ai/k8sgpt/issues/1512)) ([dfcc5dc](https://github.com/k8sgpt-ai/k8sgpt/commit/dfcc5dc5a15a3d59a7f6317944784e3ecd86fb50))
|
||||
* **deps:** update docker/build-push-action digest to 2634353 ([#1517](https://github.com/k8sgpt-ai/k8sgpt/issues/1517)) ([7dfe8be](https://github.com/k8sgpt-ai/k8sgpt/commit/7dfe8bef0face65f607475a6620923fdfed57961))
|
||||
* **deps:** update softprops/action-gh-release digest to 72f2c25 ([#1526](https://github.com/k8sgpt-ai/k8sgpt/issues/1526)) ([5947876](https://github.com/k8sgpt-ai/k8sgpt/commit/5947876e4942729eea883937faf5e2b47d1f16ec))
|
||||
* **deps:** update softprops/action-gh-release digest to d5382d3 ([#1525](https://github.com/k8sgpt-ai/k8sgpt/issues/1525)) ([6b9f346](https://github.com/k8sgpt-ai/k8sgpt/commit/6b9f346bf668ed3517b23b99000611ea14afafe2))
|
||||
* model access ([#1529](https://github.com/k8sgpt-ai/k8sgpt/issues/1529)) ([be4fb1c](https://github.com/k8sgpt-ai/k8sgpt/commit/be4fb1cc034d9c3843cf3e9912a26e05bd54c146))
|
||||
|
||||
## [0.4.17](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.16...v0.4.17) (2025-05-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adding fixes for Messages API issue 1391 ([#1504](https://github.com/k8sgpt-ai/k8sgpt/issues/1504)) ([b2241c0](https://github.com/k8sgpt-ai/k8sgpt/commit/b2241c03c975aeab02897d73e57cd351f60f3af3))
|
||||
* new job analyzer ([#1506](https://github.com/k8sgpt-ai/k8sgpt/issues/1506)) ([0b7ddf5](https://github.com/k8sgpt-ai/k8sgpt/commit/0b7ddf5e3b93e56ea92dfb6447e97c067cad9e54))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* align documentation to reflect default analyzers properly ([#1498](https://github.com/k8sgpt-ai/k8sgpt/issues/1498)) ([7e375a3](https://github.com/k8sgpt-ai/k8sgpt/commit/7e375a30bee24198f9221e4a4aea17fcd2fe005c))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1454](https://github.com/k8sgpt-ai/k8sgpt/issues/1454)) ([d0f0364](https://github.com/k8sgpt-ai/k8sgpt/commit/d0f03641ae372a00cd0eca1f41ef30a988d436bc))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1500](https://github.com/k8sgpt-ai/k8sgpt/issues/1500)) ([d308c51](https://github.com/k8sgpt-ai/k8sgpt/commit/d308c511fbe06e012c641dfa08c4dcf4181b243a))
|
||||
* panic in k8sgpt auth update ([#1497](https://github.com/k8sgpt-ai/k8sgpt/issues/1497)) ([cae94e7](https://github.com/k8sgpt-ai/k8sgpt/commit/cae94e7b6df1684a3b61af3e7aa0f4e68e8df594))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/setup-go digest to d35c59a ([#1495](https://github.com/k8sgpt-ai/k8sgpt/issues/1495)) ([e76bdb0](https://github.com/k8sgpt-ai/k8sgpt/commit/e76bdb0c23b7d23972d99661c8fe1bffe5f9f398))
|
||||
* **deps:** update golangci/golangci-lint-action action to v8 ([#1490](https://github.com/k8sgpt-ai/k8sgpt/issues/1490)) ([1e57b77](https://github.com/k8sgpt-ai/k8sgpt/commit/1e57b7774c20bda4ae0b0d765278bcd3504cfb33))
|
||||
* golangci lint ([#1508](https://github.com/k8sgpt-ai/k8sgpt/issues/1508)) ([4faf77d](https://github.com/k8sgpt-ai/k8sgpt/commit/4faf77d91a3da8fdd6166ec1c381a151e5846057))
|
||||
|
||||
## [0.4.16](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.15...v0.4.16) (2025-05-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for Amazon Bedrock Inference Profiles ([#1492](https://github.com/k8sgpt-ai/k8sgpt/issues/1492)) ([21bc76e](https://github.com/k8sgpt-ai/k8sgpt/commit/21bc76e5b77524b48f09ef6707204742dcd879a7))
|
||||
* enhancement of deployment analyzer ([#1406](https://github.com/k8sgpt-ai/k8sgpt/issues/1406)) ([61b60d5](https://github.com/k8sgpt-ai/k8sgpt/commit/61b60d5768b54f98232dcc415e89aa38987dc6e3))
|
||||
* supported regions govcloud ([#1483](https://github.com/k8sgpt-ai/k8sgpt/issues/1483)) ([752a16c](https://github.com/k8sgpt-ai/k8sgpt/commit/752a16c40728f42f10ab6c3177cb7e24f44db339))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to 0f33e8f ([#1484](https://github.com/k8sgpt-ai/k8sgpt/issues/1484)) ([6a81d2c](https://github.com/k8sgpt-ai/k8sgpt/commit/6a81d2c140f00a405b651d6c6dae5e343ffddb4f))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update docker/build-push-action digest to 14487ce ([#1472](https://github.com/k8sgpt-ai/k8sgpt/issues/1472)) ([81da402](https://github.com/k8sgpt-ai/k8sgpt/commit/81da402d46e1a1db83a41b717dfb23eb07d2e919))
|
||||
* **deps:** update golangci/golangci-lint-action digest to 9fae48a ([#1489](https://github.com/k8sgpt-ai/k8sgpt/issues/1489)) ([d5341f3](https://github.com/k8sgpt-ai/k8sgpt/commit/d5341f3c0019c1114254ac05f00c743a0354ec0b))
|
||||
|
||||
## [0.4.15](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.14...v0.4.15) (2025-04-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added token for goreleaser ([#1476](https://github.com/k8sgpt-ai/k8sgpt/issues/1476)) ([85935a4](https://github.com/k8sgpt-ai/k8sgpt/commit/85935a46d8f137b0339435cf19ce7f83ead97f8c))
|
||||
|
||||
## [0.4.14](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.13...v0.4.14) (2025-04-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add MCP support ([#1471](https://github.com/k8sgpt-ai/k8sgpt/issues/1471)) ([e41ffd8](https://github.com/k8sgpt-ai/k8sgpt/commit/e41ffd80d01ce7ae1fac9ce7e07344020d8bf914))
|
||||
* using modelName will calling completion ([#1469](https://github.com/k8sgpt-ai/k8sgpt/issues/1469)) ([f603948](https://github.com/k8sgpt-ai/k8sgpt/commit/f603948935f1c4cb171378634714577205de7b08))
|
||||
|
||||
## [0.4.13](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.12...v0.4.13) (2025-04-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* slack announce ([#1466](https://github.com/k8sgpt-ai/k8sgpt/issues/1466)) ([3b6ad06](https://github.com/k8sgpt-ai/k8sgpt/commit/3b6ad06de1121c870fb486e0fe2bd1f87be16627))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reverse hpa ScalingLimited error condition ([#1366](https://github.com/k8sgpt-ai/k8sgpt/issues/1366)) ([ebb0373](https://github.com/k8sgpt-ai/k8sgpt/commit/ebb0373f69ad64a6cc43d0695d07e1d076c6366e))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update softprops/action-gh-release digest to da05d55 ([#1464](https://github.com/k8sgpt-ai/k8sgpt/issues/1464)) ([4434699](https://github.com/k8sgpt-ai/k8sgpt/commit/443469960a6b6791e358ee0a97e4c1dc5c3018e6))
|
||||
|
||||
## [0.4.12](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.11...v0.4.12) (2025-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* new analyzers ([#1459](https://github.com/k8sgpt-ai/k8sgpt/issues/1459)) ([a128906](https://github.com/k8sgpt-ai/k8sgpt/commit/a128906136431189812d4d2dea68ea98cbfe5eeb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module golang.org/x/net to v0.38.0 [security] ([#1462](https://github.com/k8sgpt-ai/k8sgpt/issues/1462)) ([e588fc3](https://github.com/k8sgpt-ai/k8sgpt/commit/e588fc316d29a29a7dde6abe2302833b38f1d302))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update codecov/codecov-action digest to ad3126e ([#1456](https://github.com/k8sgpt-ai/k8sgpt/issues/1456)) ([0553b98](https://github.com/k8sgpt-ai/k8sgpt/commit/0553b984b7c87b345f171bf6e5d632d890db689c))
|
||||
|
||||
## [0.4.11](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.10...v0.4.11) (2025-04-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add verbose flag to enable detailed output ([#1420](https://github.com/k8sgpt-ai/k8sgpt/issues/1420)) ([a79224e](https://github.com/k8sgpt-ai/k8sgpt/commit/a79224e2bf96f458dbc96404c8f4847970e8d2ef))
|
||||
* call bedrock with inference profile ([#1449](https://github.com/k8sgpt-ai/k8sgpt/issues/1449)) ([91d423b](https://github.com/k8sgpt-ai/k8sgpt/commit/91d423b147ca18cda7d54ff19349938a894ecb85))
|
||||
* improved test coverage ([#1455](https://github.com/k8sgpt-ai/k8sgpt/issues/1455)) ([80904e3](https://github.com/k8sgpt-ai/k8sgpt/commit/80904e3063b00b0536171b7b62b938938b20825a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* config ai provider in query ([#1457](https://github.com/k8sgpt-ai/k8sgpt/issues/1457)) ([df17e3e](https://github.com/k8sgpt-ai/k8sgpt/commit/df17e3e728591e974703527dff86de882af17790))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1447](https://github.com/k8sgpt-ai/k8sgpt/issues/1447)) ([969fe99](https://github.com/k8sgpt-ai/k8sgpt/commit/969fe99b3320c313f1c97133cdffb668a00d5fb5))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1453](https://github.com/k8sgpt-ai/k8sgpt/issues/1453)) ([cf6f928](https://github.com/k8sgpt-ai/k8sgpt/commit/cf6f9289e13ee729c24968fd771c901f412e8db7))
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
* fix the slack invite link ([#1450](https://github.com/k8sgpt-ai/k8sgpt/issues/1450)) ([9ce3346](https://github.com/k8sgpt-ai/k8sgpt/commit/9ce33469d85aa0829e995e4b404ae85734124fb4))
|
||||
|
||||
## [0.4.10](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.9...v0.4.10) (2025-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add a naive support of bedrock inference profile ([#1446](https://github.com/k8sgpt-ai/k8sgpt/issues/1446)) ([78ffa59](https://github.com/k8sgpt-ai/k8sgpt/commit/78ffa5904addf71caf04554966437b14351f21e5))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1417](https://github.com/k8sgpt-ai/k8sgpt/issues/1417)) ([ce4b3c2](https://github.com/k8sgpt-ai/k8sgpt/commit/ce4b3c2e7d0762093506d9010eceb47a2dcdf5bc))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.17.3 [security] ([#1448](https://github.com/k8sgpt-ai/k8sgpt/issues/1448)) ([060a3b2](https://github.com/k8sgpt-ai/k8sgpt/commit/060a3b2a26f117827090697eb599cd51a44125e6))
|
||||
* pod analyzer catches errors when containers are in Terminated state ([#1438](https://github.com/k8sgpt-ai/k8sgpt/issues/1438)) ([dceda9a](https://github.com/k8sgpt-ai/k8sgpt/commit/dceda9a6a16a914b916c478ecd0b4c8ed0e19c40))
|
||||
|
||||
## [0.4.9](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.8...v0.4.9) (2025-04-08)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** pin dependencies ([#1440](https://github.com/k8sgpt-ai/k8sgpt/issues/1440)) ([a5574ee](https://github.com/k8sgpt-ai/k8sgpt/commit/a5574ee49d530960a515c419f4875cf02cb36fb3))
|
||||
* fixing ([#1437](https://github.com/k8sgpt-ai/k8sgpt/issues/1437)) ([f68ff0e](https://github.com/k8sgpt-ai/k8sgpt/commit/f68ff0efee9bad5f8368c83800611fa9acbc53d7))
|
||||
|
||||
## [0.4.8](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.7...v0.4.8) (2025-04-07)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* removed krew release ([#1434](https://github.com/k8sgpt-ai/k8sgpt/issues/1434)) ([39ae2aa](https://github.com/k8sgpt-ai/k8sgpt/commit/39ae2aa6351d6a77e0b45ad15b0d10b86a33f3be))
|
||||
|
||||
## [0.4.7](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.6...v0.4.7) (2025-04-07)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/upload-artifact digest to ea165f8 ([#1425](https://github.com/k8sgpt-ai/k8sgpt/issues/1425)) ([9bffc7c](https://github.com/k8sgpt-ai/k8sgpt/commit/9bffc7cff776733f6d05669e6c02f594ee2db261))
|
||||
* fixing build ([#1431](https://github.com/k8sgpt-ai/k8sgpt/issues/1431)) ([c5fe2c6](https://github.com/k8sgpt-ai/k8sgpt/commit/c5fe2c68d18d4fd713b3e638066327ad586d1871))
|
||||
|
||||
## [0.4.6](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.5...v0.4.6) (2025-04-07)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** pin docker/build-push-action action to 471d1dc ([#1428](https://github.com/k8sgpt-ai/k8sgpt/issues/1428)) ([5086ccd](https://github.com/k8sgpt-ai/k8sgpt/commit/5086ccd65942ebb9a37bd2c3a48d16c4be99e8c1))
|
||||
* fixing docker build push action ([#1426](https://github.com/k8sgpt-ai/k8sgpt/issues/1426)) ([1681aad](https://github.com/k8sgpt-ai/k8sgpt/commit/1681aadac106c608de9774ebfd7ea9df20eed482))
|
||||
* updated actor for login ([#1430](https://github.com/k8sgpt-ai/k8sgpt/issues/1430)) ([b626102](https://github.com/k8sgpt-ai/k8sgpt/commit/b6261026f8b41e505359a52c18bebec7ef5079f9))
|
||||
|
||||
## [0.4.5](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.4...v0.4.5) (2025-04-07)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* fix workflows ([#1423](https://github.com/k8sgpt-ai/k8sgpt/issues/1423)) ([3dbc9e1](https://github.com/k8sgpt-ai/k8sgpt/commit/3dbc9e1a20a3a55971733d990ecd39e798a804e9))
|
||||
|
||||
## [0.4.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.3...v0.4.4) (2025-04-06)
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update docker/setup-buildx-action digest to b5ca514 ([#1371](https://github.com/k8sgpt-ai/k8sgpt/issues/1371)) ([d4de5d9](https://github.com/k8sgpt-ai/k8sgpt/commit/d4de5d9e3fdd1cc4c7d6fc067a7426fef1d32c1d))
|
||||
* **deps:** update module github.com/docker/docker to v28 ([#1376](https://github.com/k8sgpt-ai/k8sgpt/issues/1376)) ([68ddac0](https://github.com/k8sgpt-ai/k8sgpt/commit/68ddac008955933ffa27c2c4e46d286d9a26e100))
|
||||
* updating deps ([#1422](https://github.com/k8sgpt-ai/k8sgpt/issues/1422)) ([5b7fb7e](https://github.com/k8sgpt-ai/k8sgpt/commit/5b7fb7e6199635e109c1bf7355bc11ff6f60071b))
|
||||
|
||||
## [0.4.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.2...v0.4.3) (2025-04-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1363](https://github.com/k8sgpt-ai/k8sgpt/issues/1363)) ([e4861e9](https://github.com/k8sgpt-ai/k8sgpt/commit/e4861e9e2d631652b82768567afb9ba174114134))
|
||||
* prometheus UTF8Validation ([#1404](https://github.com/k8sgpt-ai/k8sgpt/issues/1404)) ([3c353b0](https://github.com/k8sgpt-ai/k8sgpt/commit/3c353b0e931028f3be3b229518cf86d24422a29d))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* added new AmazonBedrock model ([#1390](https://github.com/k8sgpt-ai/k8sgpt/issues/1390)) ([ad2c90a](https://github.com/k8sgpt-ai/k8sgpt/commit/ad2c90a129074a13dac4fdd8e918d8e26159c7a1))
|
||||
* **deps:** pin golangci/golangci-lint-action action to 1481404 ([#1415](https://github.com/k8sgpt-ai/k8sgpt/issues/1415)) ([e231032](https://github.com/k8sgpt-ai/k8sgpt/commit/e231032e1bec1d2d25cb03b35e701aa86a61d5ee))
|
||||
* **deps:** update goreleaser/goreleaser-action digest to 9c156ee ([#1411](https://github.com/k8sgpt-ai/k8sgpt/issues/1411)) ([c823de1](https://github.com/k8sgpt-ai/k8sgpt/commit/c823de12e6b6efcf9f5639665aac602ed85ae31d))
|
||||
* linter ([#1414](https://github.com/k8sgpt-ai/k8sgpt/issues/1414)) ([f0b18cf](https://github.com/k8sgpt-ai/k8sgpt/commit/f0b18cfb1cd418b94b448d3b9de43f03841c92bb))
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
* add table of contents and cleanup ([#1413](https://github.com/k8sgpt-ai/k8sgpt/issues/1413)) ([a31d07c](https://github.com/k8sgpt-ai/k8sgpt/commit/a31d07c802694d3455b665382ff12a2abc3e0ef7))
|
||||
* remove extra dollar sign in README.md ([#1410](https://github.com/k8sgpt-ai/k8sgpt/issues/1410)) ([a962741](https://github.com/k8sgpt-ai/k8sgpt/commit/a962741220bf98e159f14895d01cd596a7691f87))
|
||||
|
||||
## [0.4.2](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.1...v0.4.2) (2025-03-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* old sonnet ([#1408](https://github.com/k8sgpt-ai/k8sgpt/issues/1408)) ([e5817f9](https://github.com/k8sgpt-ai/k8sgpt/commit/e5817f9e557f4f97b016a0a7b7674342c3a1773e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update k8s.io/utils digest to 1f6e0b7 ([#1405](https://github.com/k8sgpt-ai/k8sgpt/issues/1405)) ([f5eaf81](https://github.com/k8sgpt-ai/k8sgpt/commit/f5eaf817f0cf2b732013e67e94c758a225c35ba6))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/setup-go digest to 0aaccfd ([#1401](https://github.com/k8sgpt-ai/k8sgpt/issues/1401)) ([81d4aaf](https://github.com/k8sgpt-ai/k8sgpt/commit/81d4aaf402647bf4bcbc618fd82f9518cf3a5b4d))
|
||||
* **deps:** update actions/upload-artifact digest to ea165f8 ([#1402](https://github.com/k8sgpt-ai/k8sgpt/issues/1402)) ([eb381b8](https://github.com/k8sgpt-ai/k8sgpt/commit/eb381b8087bbb3216d9bcdcc88a71fbad9e31e41))
|
||||
* **deps:** update docker/login-action digest to 74a5d14 ([#1397](https://github.com/k8sgpt-ai/k8sgpt/issues/1397)) ([fdf8e7a](https://github.com/k8sgpt-ai/k8sgpt/commit/fdf8e7a95a6667b782e1e347a3b1d2fb0f2aafde))
|
||||
* fix error ([#1403](https://github.com/k8sgpt-ai/k8sgpt/issues/1403)) ([288ca86](https://github.com/k8sgpt-ai/k8sgpt/commit/288ca862b3aaf942e58aa0dad0e15e2fda84780f))
|
||||
|
||||
## [0.4.1](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.0...v0.4.1) (2025-03-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add amazon bedrock nova pro and nova lite models ([#1383](https://github.com/k8sgpt-ai/k8sgpt/issues/1383)) ([aa1e237](https://github.com/k8sgpt-ai/k8sgpt/commit/aa1e237ebb8c816383561c9b3e6a1ca0ddea8f78))
|
||||
* add custom restful backend for complex scenarios (e.g, rag) ([#1228](https://github.com/k8sgpt-ai/k8sgpt/issues/1228)) ([7540e00](https://github.com/k8sgpt-ai/k8sgpt/commit/7540e0084e0c0c44fc52ed9a906b76f9f2e6a981))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update default model to gpt-4o for improved performance and cost efficiency ([#1332](https://github.com/k8sgpt-ai/k8sgpt/issues/1332)) ([4e39cb6](https://github.com/k8sgpt-ai/k8sgpt/commit/4e39cb65b3a7fc0d1c057c647794346e072d3fd0))
|
||||
* **deps:** update module golang.org/x/net to v0.36.0 [security] ([#1395](https://github.com/k8sgpt-ai/k8sgpt/issues/1395)) ([eb7b36a](https://github.com/k8sgpt-ai/k8sgpt/commit/eb7b36aa2764bc460ffc29a0aee18abe3631c2ed))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/setup-go digest to f111f33 ([#1364](https://github.com/k8sgpt-ai/k8sgpt/issues/1364)) ([f2fdfd8](https://github.com/k8sgpt-ai/k8sgpt/commit/f2fdfd8dcaae6f57378d50396c4746d738d38bf2))
|
||||
* **deps:** update goreleaser/goreleaser-action digest to 90a3faa ([#1308](https://github.com/k8sgpt-ai/k8sgpt/issues/1308)) ([d6d2e3b](https://github.com/k8sgpt-ai/k8sgpt/commit/d6d2e3bc4254877c8af61aba7386706e942e3fe9))
|
||||
* **deps:** update softprops/action-gh-release digest to c95fe14 ([#1359](https://github.com/k8sgpt-ai/k8sgpt/issues/1359)) ([db5e517](https://github.com/k8sgpt-ai/k8sgpt/commit/db5e517dbb23a4cb0f203427744f4007d6e9faa8))
|
||||
|
||||
## [0.4.0](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.50...v0.4.0) (2025-03-06)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* Removal of Trivy ([#1386](https://github.com/k8sgpt-ai/k8sgpt/issues/1386))
|
||||
|
||||
### Features
|
||||
|
||||
* Removal of Trivy ([#1386](https://github.com/k8sgpt-ai/k8sgpt/issues/1386)) ([d1b2227](https://github.com/k8sgpt-ai/k8sgpt/commit/d1b2227ff9a8ef42bf63c83e289fbd801706821e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [Bug] Filter PolicyReport ignores namespace flag ([#1355](https://github.com/k8sgpt-ai/k8sgpt/issues/1355)) ([9dcb21e](https://github.com/k8sgpt-ai/k8sgpt/commit/9dcb21e160233eb120ccf50f9b9b80c145d0e01a))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* Adding region ([#1388](https://github.com/k8sgpt-ai/k8sgpt/issues/1388)) ([4f4f4f1](https://github.com/k8sgpt-ai/k8sgpt/commit/4f4f4f13a065ca7add283088c93777f78dcea228))
|
||||
* **deps:** update actions/upload-artifact digest to 4cec3d8 ([#1378](https://github.com/k8sgpt-ai/k8sgpt/issues/1378)) ([093975e](https://github.com/k8sgpt-ai/k8sgpt/commit/093975e50ddadeab70a7c4f544df8351ac9758a2))
|
||||
* **deps:** update codecov/codecov-action digest to 0565863 ([#1387](https://github.com/k8sgpt-ai/k8sgpt/issues/1387)) ([2a6f485](https://github.com/k8sgpt-ai/k8sgpt/commit/2a6f48500c4567519453fc51ea070f5e407d3cfb))
|
||||
* **deps:** update docker/build-push-action digest to 471d1dc ([#1358](https://github.com/k8sgpt-ai/k8sgpt/issues/1358)) ([f2e3b9a](https://github.com/k8sgpt-ai/k8sgpt/commit/f2e3b9a8a72c4df32713197e50756e37e1302ff9))
|
||||
* remediating security issue ([#1381](https://github.com/k8sgpt-ai/k8sgpt/issues/1381)) ([1f95358](https://github.com/k8sgpt-ai/k8sgpt/commit/1f953585c91f8a208db3b37440e4d458b8d821eb))
|
||||
|
||||
## [0.3.50](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.49...v0.3.50) (2025-02-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* rework to how bedrock data models are structured and accessed ([#1369](https://github.com/k8sgpt-ai/k8sgpt/issues/1369)) ([7dadea2](https://github.com/k8sgpt-ai/k8sgpt/commit/7dadea257007df64148f1e47f7960d1d30df67b2))
|
||||
|
||||
## [0.3.49](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.48...v0.3.49) (2025-02-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update all non-major dependencies ([#1335](https://github.com/k8sgpt-ai/k8sgpt/issues/1335)) ([8cd3b29](https://github.com/k8sgpt-ai/k8sgpt/commit/8cd3b2985e4cd61711497fb0436e72b6b8aa3162))
|
||||
* **deps:** update k8s.io/utils digest to 24370be ([#1344](https://github.com/k8sgpt-ai/k8sgpt/issues/1344)) ([fcc8563](https://github.com/k8sgpt-ai/k8sgpt/commit/fcc8563e4eba9bf45d49901b7287d311b93372c2))
|
||||
* **deps:** update module golang.org/x/net to v0.33.0 [security] ([#1354](https://github.com/k8sgpt-ai/k8sgpt/issues/1354)) ([5de4f77](https://github.com/k8sgpt-ai/k8sgpt/commit/5de4f7704a856fd7db7b2f800bda40c5beb9333b))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1336](https://github.com/k8sgpt-ai/k8sgpt/issues/1336)) ([19abbef](https://github.com/k8sgpt-ai/k8sgpt/commit/19abbef9a3112ceb060ac3fd772e2e4f62f19f84))
|
||||
* prevent npe by handling checking error in NewAnalysis call ([#1365](https://github.com/k8sgpt-ai/k8sgpt/issues/1365)) ([83672fa](https://github.com/k8sgpt-ai/k8sgpt/commit/83672fa768887dd1c6f4dc12a92c3444f100c4f6))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/setup-go digest to 3041bf5 ([#1347](https://github.com/k8sgpt-ai/k8sgpt/issues/1347)) ([939e067](https://github.com/k8sgpt-ai/k8sgpt/commit/939e0672aaaa5538cd58bb171f1e5d1c07831651))
|
||||
* **deps:** update actions/upload-artifact digest to 65c4c4a ([#1350](https://github.com/k8sgpt-ai/k8sgpt/issues/1350)) ([c506a4b](https://github.com/k8sgpt-ai/k8sgpt/commit/c506a4b441e24052398c00c93d96806cec1b9f75))
|
||||
* **deps:** update codecov/codecov-action digest to 13ce06b ([#1342](https://github.com/k8sgpt-ai/k8sgpt/issues/1342)) ([990d723](https://github.com/k8sgpt-ai/k8sgpt/commit/990d7239091b368178e06af60e4dc0e897fc8236))
|
||||
* **deps:** update docker/setup-buildx-action digest to 6524bf6 ([#1349](https://github.com/k8sgpt-ai/k8sgpt/issues/1349)) ([2918556](https://github.com/k8sgpt-ai/k8sgpt/commit/2918556793316ea4f5a319c9aa51c1fec12ede85))
|
||||
* fix typo in "completion" ([#1362](https://github.com/k8sgpt-ai/k8sgpt/issues/1362)) ([06b8f78](https://github.com/k8sgpt-ai/k8sgpt/commit/06b8f78150308c1f6023747fa34826e038d6bc3a))
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
* fix broken schema link in README.md ([#1373](https://github.com/k8sgpt-ai/k8sgpt/issues/1373)) ([076ca2f](https://github.com/k8sgpt-ai/k8sgpt/commit/076ca2f14832cf83e43c465c377ef21825218b2f))
|
||||
|
||||
## [0.3.48](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.47...v0.3.48) (2024-12-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* fixed missing cache params ([#1340](https://github.com/k8sgpt-ai/k8sgpt/issues/1340)) ([1363219](https://github.com/k8sgpt-ai/k8sgpt/commit/1363219b1b94e157ef03c53eba8838b7cef559b4))
|
||||
|
||||
## [0.3.47](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.46...v0.3.47) (2024-12-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add new AWS Bedrock model ids ([#1330](https://github.com/k8sgpt-ai/k8sgpt/issues/1330)) ([a12aa07](https://github.com/k8sgpt-ai/k8sgpt/commit/a12aa07b1a2e34c5106b7b930b29b0c97b172dc4))
|
||||
* adds interplex as a caching provider ([#1328](https://github.com/k8sgpt-ai/k8sgpt/issues/1328)) ([d6d80ee](https://github.com/k8sgpt-ai/k8sgpt/commit/d6d80ee86083643d9b91457791bfc77ef475e82e))
|
||||
* dump ([#1322](https://github.com/k8sgpt-ai/k8sgpt/issues/1322)) ([da266b3](https://github.com/k8sgpt-ai/k8sgpt/commit/da266b3c82ca8b3e96461be688a9f30e408568fe))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add maxTokens to serve mode ([#1280](https://github.com/k8sgpt-ai/k8sgpt/issues/1280)) ([a50375c](https://github.com/k8sgpt-ai/k8sgpt/commit/a50375c9605a87546a0fcbcacabe5482fdfa1c2c))
|
||||
* **deps:** update all non-major dependencies ([#1323](https://github.com/k8sgpt-ai/k8sgpt/issues/1323)) ([b3f60b2](https://github.com/k8sgpt-ai/k8sgpt/commit/b3f60b2d2018d4bede3918adcb3547ef2acf6688))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1303](https://github.com/k8sgpt-ai/k8sgpt/issues/1303)) ([2da0573](https://github.com/k8sgpt-ai/k8sgpt/commit/2da057360b378d34126e1480ade0686f104e3ace))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1321](https://github.com/k8sgpt-ai/k8sgpt/issues/1321)) ([69c67bd](https://github.com/k8sgpt-ai/k8sgpt/commit/69c67bd1d9d4404816a8b7a00c98499729f2185f))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1326](https://github.com/k8sgpt-ai/k8sgpt/issues/1326)) ([5514ebb](https://github.com/k8sgpt-ai/k8sgpt/commit/5514ebb53b79b5bac0fc861ffdebc9399fe87b62))
|
||||
* update OpenAI API key generation URL to reflect new platform link ([#1331](https://github.com/k8sgpt-ai/k8sgpt/issues/1331)) ([ec5e42b](https://github.com/k8sgpt-ai/k8sgpt/commit/ec5e42b8f43e90632bb62dd89cc6aa3665e0f60d))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update all non-major dependencies ([#1327](https://github.com/k8sgpt-ai/k8sgpt/issues/1327)) ([a841568](https://github.com/k8sgpt-ai/k8sgpt/commit/a841568a9c4c0012291cf8f4248250192b72a383))
|
||||
* **deps:** update codecov/codecov-action action to v5 ([#1324](https://github.com/k8sgpt-ai/k8sgpt/issues/1324)) ([cb1e1ff](https://github.com/k8sgpt-ai/k8sgpt/commit/cb1e1ffede1d3086d54157142c6803341e560ca8))
|
||||
* **deps:** update codecov/codecov-action digest to 015f24e ([#1325](https://github.com/k8sgpt-ai/k8sgpt/issues/1325)) ([4d7eb0f](https://github.com/k8sgpt-ai/k8sgpt/commit/4d7eb0f6226fc50f58b5c2fff7534dd16e2ca378))
|
||||
* **deps:** update docker/build-push-action action to v6 ([#1294](https://github.com/k8sgpt-ai/k8sgpt/issues/1294)) ([f37d923](https://github.com/k8sgpt-ai/k8sgpt/commit/f37d92391877819c6d26a993ab58bc0c49fb3b66))
|
||||
* **deps:** update docker/build-push-action digest to 48aba3b ([#1333](https://github.com/k8sgpt-ai/k8sgpt/issues/1333)) ([c21ba86](https://github.com/k8sgpt-ai/k8sgpt/commit/c21ba86237db651086c0a37abc3454db513e505b))
|
||||
* **deps:** update rajatjindal/krew-release-bot action to v0.0.47 ([#1317](https://github.com/k8sgpt-ai/k8sgpt/issues/1317)) ([896a53b](https://github.com/k8sgpt-ai/k8sgpt/commit/896a53be8394c490e2d34f151de44c3663dddf5b))
|
||||
|
||||
## [0.3.46](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.45...v0.3.46) (2024-11-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* reverting the cncf runners ([#1319](https://github.com/k8sgpt-ai/k8sgpt/issues/1319)) ([ad86e7a](https://github.com/k8sgpt-ai/k8sgpt/commit/ad86e7aa39995c492437627dbd9f89f152f11f2c))
|
||||
* switching to higher spec runners ([#1312](https://github.com/k8sgpt-ai/k8sgpt/issues/1312)) ([5f7d9de](https://github.com/k8sgpt-ai/k8sgpt/commit/5f7d9de46a521463cedc901b729fe27f8d86f381))
|
||||
* testupdate ([#1315](https://github.com/k8sgpt-ai/k8sgpt/issues/1315)) ([7dcdfc8](https://github.com/k8sgpt-ai/k8sgpt/commit/7dcdfc83d2461e4342ded5fa80493936b70f64a1))
|
||||
* updated runners to enterprise ([#1318](https://github.com/k8sgpt-ai/k8sgpt/issues/1318)) ([1ae70e8](https://github.com/k8sgpt-ai/k8sgpt/commit/1ae70e806e2609c8fb964f0a577304d07b365cae))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update actions/setup-go digest to 41dfa10 ([#1284](https://github.com/k8sgpt-ai/k8sgpt/issues/1284)) ([2ce8450](https://github.com/k8sgpt-ai/k8sgpt/commit/2ce8450e03986904a7ffe7afac4b5ba777c67c57))
|
||||
* **deps:** update softprops/action-gh-release action to v2 ([#1295](https://github.com/k8sgpt-ai/k8sgpt/issues/1295)) ([b6b3d0c](https://github.com/k8sgpt-ai/k8sgpt/commit/b6b3d0c8566b0dbd9cb0e5f59c8493e4343e0106))
|
||||
|
||||
## [0.3.45](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.44...v0.3.45) (2024-11-10)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
We're happy that you want to contribute to this project. Please read the sections to make the process as smooth as possible.
|
||||
|
||||
## Requirements
|
||||
- Golang `1.20`
|
||||
- Golang `1.24+`
|
||||
- An OpenAI API key
|
||||
* OpenAI API keys can be obtained from [OpenAI](https://platform.openai.com/account/api-keys)
|
||||
* You can set the API key for k8sgpt using `./k8sgpt auth key`
|
||||
|
||||
482
MCP.md
Normal file
482
MCP.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# K8sGPT Model Context Protocol (MCP) Server
|
||||
|
||||
K8sGPT provides a Model Context Protocol (MCP) server that exposes Kubernetes cluster operations as standardized tools, resources, and prompts for AI assistants like Claude, ChatGPT, and other MCP-compatible clients.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [What is MCP?](#what-is-mcp)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Server Modes](#server-modes)
|
||||
- [Available Tools](#available-tools)
|
||||
- [Available Resources](#available-resources)
|
||||
- [Available Prompts](#available-prompts)
|
||||
- [Usage Examples](#usage-examples)
|
||||
- [Integration with AI Assistants](#integration-with-ai-assistants)
|
||||
- [HTTP API Reference](#http-api-reference)
|
||||
|
||||
## What is MCP?
|
||||
|
||||
The Model Context Protocol (MCP) is an open standard that enables AI assistants to securely connect to external data sources and tools. K8sGPT's MCP server exposes Kubernetes operations through this standardized interface, allowing AI assistants to:
|
||||
|
||||
- Analyze cluster health and issues
|
||||
- Query Kubernetes resources
|
||||
- Access pod logs and events
|
||||
- Get troubleshooting guidance
|
||||
- Manage analyzer filters
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Start the MCP Server
|
||||
|
||||
**Stdio mode (for local AI assistants):**
|
||||
```bash
|
||||
k8sgpt serve --mcp
|
||||
```
|
||||
|
||||
**HTTP mode (for network access):**
|
||||
```bash
|
||||
k8sgpt serve --mcp --mcp-http --mcp-port 8089
|
||||
```
|
||||
|
||||
### Test with curl
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8089/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list"
|
||||
}'
|
||||
```
|
||||
|
||||
## Server Modes
|
||||
|
||||
### Stdio Mode (Default)
|
||||
|
||||
Used by local AI assistants like Claude Desktop:
|
||||
|
||||
```bash
|
||||
k8sgpt serve --mcp
|
||||
```
|
||||
|
||||
Configure in your MCP client (e.g., Claude Desktop's `claude_desktop_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"k8sgpt": {
|
||||
"command": "k8sgpt",
|
||||
"args": ["serve", "--mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Mode
|
||||
|
||||
Used for network access and webhooks:
|
||||
|
||||
```bash
|
||||
k8sgpt serve --mcp --mcp-http --mcp-port 8089
|
||||
```
|
||||
|
||||
The server runs in stateless mode, so no session management is required. Each request is independent.
|
||||
|
||||
## Available Tools
|
||||
|
||||
The MCP server exposes 12 tools for Kubernetes operations:
|
||||
|
||||
### Cluster Analysis
|
||||
|
||||
**analyze**
|
||||
- Analyze Kubernetes resources for issues and problems
|
||||
- Parameters:
|
||||
- `namespace` (optional): Namespace to analyze
|
||||
- `explain` (optional): Get AI explanations for issues
|
||||
- `filters` (optional): Comma-separated list of analyzers to use
|
||||
|
||||
**cluster-info**
|
||||
- Get Kubernetes cluster information and version
|
||||
|
||||
### Resource Management
|
||||
|
||||
**list-resources**
|
||||
- List Kubernetes resources of a specific type
|
||||
- Parameters:
|
||||
- `resourceType` (required): Type of resource (pods, deployments, services, nodes, jobs, cronjobs, statefulsets, daemonsets, replicasets, configmaps, secrets, ingresses, pvcs, pvs)
|
||||
- `namespace` (optional): Namespace to query
|
||||
- `labelSelector` (optional): Label selector for filtering
|
||||
|
||||
**get-resource**
|
||||
- Get detailed information about a specific Kubernetes resource
|
||||
- Parameters:
|
||||
- `resourceType` (required): Type of resource
|
||||
- `name` (required): Resource name
|
||||
- `namespace` (optional): Namespace
|
||||
|
||||
**list-namespaces**
|
||||
- List all namespaces in the cluster
|
||||
|
||||
### Debugging and Troubleshooting
|
||||
|
||||
**get-logs**
|
||||
- Get logs from a pod container
|
||||
- Parameters:
|
||||
- `podName` (required): Name of the pod
|
||||
- `namespace` (optional): Namespace
|
||||
- `container` (optional): Container name
|
||||
- `tail` (optional): Number of lines to show
|
||||
- `previous` (optional): Show logs from previous container instance
|
||||
- `sinceSeconds` (optional): Show logs from last N seconds
|
||||
|
||||
**list-events**
|
||||
- List Kubernetes events for debugging
|
||||
- Parameters:
|
||||
- `namespace` (optional): Namespace to query
|
||||
- `involvedObjectName` (optional): Filter by object name
|
||||
- `involvedObjectKind` (optional): Filter by object kind
|
||||
|
||||
### Analyzer Management
|
||||
|
||||
**list-filters**
|
||||
- List all available and active analyzers/filters
|
||||
|
||||
**add-filters**
|
||||
- Add filters to enable specific analyzers
|
||||
- Parameters:
|
||||
- `filters` (required): Comma-separated list of analyzer names
|
||||
|
||||
**remove-filters**
|
||||
- Remove filters to disable specific analyzers
|
||||
- Parameters:
|
||||
- `filters` (required): Comma-separated list of analyzer names
|
||||
|
||||
### Integrations
|
||||
|
||||
**list-integrations**
|
||||
- List available integrations (Prometheus, AWS, Keda, Kyverno, etc.)
|
||||
|
||||
### Configuration
|
||||
|
||||
**config**
|
||||
- Configure K8sGPT settings including custom analyzers and cache
|
||||
|
||||
## Available Resources
|
||||
|
||||
Resources provide read-only access to cluster information:
|
||||
|
||||
**cluster-info**
|
||||
- URI: `cluster-info`
|
||||
- Get information about the Kubernetes cluster
|
||||
|
||||
**namespaces**
|
||||
- URI: `namespaces`
|
||||
- List all namespaces in the cluster
|
||||
|
||||
**active-filters**
|
||||
- URI: `active-filters`
|
||||
- Get currently active analyzers/filters
|
||||
|
||||
## Available Prompts
|
||||
|
||||
Prompts provide guided troubleshooting workflows:
|
||||
|
||||
**troubleshoot-pod**
|
||||
- Interactive pod debugging workflow
|
||||
- Arguments:
|
||||
- `podName` (required): Name of the pod to troubleshoot
|
||||
- `namespace` (required): Namespace of the pod
|
||||
|
||||
**troubleshoot-deployment**
|
||||
- Interactive deployment debugging workflow
|
||||
- Arguments:
|
||||
- `deploymentName` (required): Name of the deployment
|
||||
- `namespace` (required): Namespace of the deployment
|
||||
|
||||
**troubleshoot-cluster**
|
||||
- General cluster troubleshooting workflow
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Analyze a Namespace
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8089/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "analyze",
|
||||
"arguments": {
|
||||
"namespace": "production",
|
||||
"explain": "true"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Example 2: List Pods
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8089/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "list-resources",
|
||||
"arguments": {
|
||||
"resourceType": "pods",
|
||||
"namespace": "default"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Example 3: Get Pod Logs
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8089/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "get-logs",
|
||||
"arguments": {
|
||||
"podName": "nginx-abc123",
|
||||
"namespace": "default",
|
||||
"tail": "100"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Example 4: Access a Resource
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8089/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"method": "resources/read",
|
||||
"params": {
|
||||
"uri": "namespaces"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Example 5: Get a Troubleshooting Prompt
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8089/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 5,
|
||||
"method": "prompts/get",
|
||||
"params": {
|
||||
"name": "troubleshoot-pod",
|
||||
"arguments": {
|
||||
"podName": "nginx-abc123",
|
||||
"namespace": "default"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Integration with AI Assistants
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Add to `claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"k8sgpt": {
|
||||
"command": "k8sgpt",
|
||||
"args": ["serve", "--mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart Claude Desktop and you'll see k8sgpt tools available in the tool selector.
|
||||
|
||||
### Custom MCP Clients
|
||||
|
||||
Any MCP-compatible client can connect to the k8sgpt server. For HTTP-based clients:
|
||||
|
||||
1. Start the server: `k8sgpt serve --mcp --mcp-http --mcp-port 8089`
|
||||
2. Connect to: `http://localhost:8089/mcp`
|
||||
3. Use standard MCP protocol methods: `tools/list`, `tools/call`, `resources/read`, `prompts/get`
|
||||
|
||||
## HTTP API Reference
|
||||
|
||||
### Endpoint
|
||||
|
||||
```
|
||||
POST http://localhost:8089/mcp
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### Request Format
|
||||
|
||||
All requests follow the JSON-RPC 2.0 format:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "method_name",
|
||||
"params": {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Discovery Methods
|
||||
|
||||
**List Tools**
|
||||
```json
|
||||
{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
|
||||
```
|
||||
|
||||
**List Resources**
|
||||
```json
|
||||
{"jsonrpc": "2.0", "id": 2, "method": "resources/list"}
|
||||
```
|
||||
|
||||
**List Prompts**
|
||||
```json
|
||||
{"jsonrpc": "2.0", "id": 3, "method": "prompts/list"}
|
||||
```
|
||||
|
||||
### Tool Invocation
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "tool_name",
|
||||
"arguments": {
|
||||
"arg1": "value1",
|
||||
"arg2": "value2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Resource Access
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 5,
|
||||
"method": "resources/read",
|
||||
"params": {
|
||||
"uri": "resource_uri"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Prompt Access
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 6,
|
||||
"method": "prompts/get",
|
||||
"params": {
|
||||
"name": "prompt_name",
|
||||
"arguments": {
|
||||
"arg1": "value1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response Format
|
||||
|
||||
Successful responses:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Error responses:
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"error": {
|
||||
"code": -32600,
|
||||
"message": "Error description"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Port
|
||||
|
||||
```bash
|
||||
k8sgpt serve --mcp --mcp-http --mcp-port 9000
|
||||
```
|
||||
|
||||
### With Specific Backend
|
||||
|
||||
```bash
|
||||
k8sgpt serve --mcp --backend openai
|
||||
```
|
||||
|
||||
### With Kubeconfig
|
||||
|
||||
```bash
|
||||
k8sgpt serve --mcp --kubeconfig ~/.kube/config
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
Verify the server is running:
|
||||
```bash
|
||||
curl http://localhost:8089/mcp
|
||||
```
|
||||
|
||||
### Permission Issues
|
||||
|
||||
Ensure your kubeconfig has appropriate cluster access:
|
||||
```bash
|
||||
kubectl cluster-info
|
||||
```
|
||||
|
||||
### Tool Errors
|
||||
|
||||
List available tools to verify names:
|
||||
```bash
|
||||
curl -X POST http://localhost:8089/mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [MCP Specification](https://modelcontextprotocol.io/)
|
||||
- [K8sGPT Documentation](https://docs.k8sgpt.ai/)
|
||||
- [MCP Go Library](https://github.com/mark3labs/mcp-go)
|
||||
6
Makefile
6
Makefile
@@ -85,6 +85,12 @@ docker-build:
|
||||
@echo "===========> Building docker image"
|
||||
docker buildx build --build-arg=VERSION="$$(git describe --tags --abbrev=0)" --build-arg=COMMIT="$$(git rev-parse --short HEAD)" --build-arg DATE="$$(date +%FT%TZ)" --platform="linux/amd64,linux/arm64" -t ${IMG} -f container/Dockerfile . --push
|
||||
|
||||
## docker-build-local: Build docker image for local testing
|
||||
.PHONY: docker-build-local
|
||||
docker-build-local:
|
||||
@echo "===========> Building docker image for local testing"
|
||||
docker build --build-arg=VERSION="$$(git describe --tags --abbrev=0)" --build-arg=COMMIT="$$(git rev-parse --short HEAD)" --build-arg DATE="$$(date +%FT%TZ)" -t k8sgpt:local -f container/Dockerfile .
|
||||
|
||||
## fmt: Run go fmt against code.
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
|
||||
214
README.md
214
README.md
@@ -21,17 +21,35 @@ It has SRE experience codified into its analyzers and helps to pull out the most
|
||||
|
||||
_Out of the box integration with OpenAI, Azure, Cohere, Amazon Bedrock, Google Gemini and local models._
|
||||
|
||||
|
||||
> **Sister project:** Check out [sympozium](https://github.com/AlexsJones/sympozium/) for managing agents in Kubernetes.
|
||||
|
||||
|
||||
<a href="https://www.producthunt.com/posts/k8sgpt?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-k8sgpt" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=389489&theme=light" alt="K8sGPT - K8sGPT gives Kubernetes Superpowers to everyone | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://hellogithub.com/repository/9dfe44c18dfb4d6fa0181baf8b2cf2e1" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=9dfe44c18dfb4d6fa0181baf8b2cf2e1&claim_uid=gqG4wmzkMrP0eFy" alt="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
|
||||
<img src="images/demo4.gif" width=650px; />
|
||||
<img src="images/demo4.gif" width="650px">
|
||||
|
||||
# Table of Contents
|
||||
- [Overview](#k8sgpt)
|
||||
- [Installation](#cli-installation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Analyzers](#analyzers)
|
||||
- [Examples](#examples)
|
||||
- [LLM AI Backends](#llm-ai-backends)
|
||||
- [Key Features](#key-features)
|
||||
- [Model Context Protocol (MCP)](#model-context-protocol-mcp)
|
||||
- [Documentation](#documentation)
|
||||
- [Contributing](#contributing)
|
||||
- [Community](#community)
|
||||
- [License](#license)
|
||||
|
||||
# CLI Installation
|
||||
|
||||
### Linux/Mac via brew
|
||||
|
||||
```sh
|
||||
$ brew install k8sgpt
|
||||
brew install k8sgpt
|
||||
```
|
||||
|
||||
or
|
||||
@@ -49,7 +67,7 @@ brew install k8sgpt
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.45/k8sgpt_386.rpm
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
|
||||
@@ -57,7 +75,7 @@ brew install k8sgpt
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.45/k8sgpt_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
</details>
|
||||
@@ -70,7 +88,7 @@ brew install k8sgpt
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.45/k8sgpt_386.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.deb
|
||||
sudo dpkg -i k8sgpt_386.deb
|
||||
```
|
||||
|
||||
@@ -81,7 +99,7 @@ sudo dpkg -i k8sgpt_386.deb
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.45/k8sgpt_amd64.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.deb
|
||||
sudo dpkg -i k8sgpt_amd64.deb
|
||||
```
|
||||
|
||||
@@ -96,7 +114,7 @@ sudo dpkg -i k8sgpt_amd64.deb
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.45/k8sgpt_386.apk
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.apk
|
||||
apk add --allow-untrusted k8sgpt_386.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -105,7 +123,7 @@ sudo dpkg -i k8sgpt_amd64.deb
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.45/k8sgpt_amd64.apk
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.apk
|
||||
apk add --allow-untrusted k8sgpt_amd64.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -133,7 +151,7 @@ If you install gcc as suggested, the problem will persist. Therefore, you need t
|
||||
|
||||
- Download the latest Windows binaries of **k8sgpt** from the [Release](https://github.com/k8sgpt-ai/k8sgpt/releases)
|
||||
tab based on your system architecture.
|
||||
- Extract the downloaded package to your desired location. Configure the system _path_ variable with the binary location
|
||||
- Extract the downloaded package to your desired location. Configure the system _PATH_ environment variable with the binary location
|
||||
|
||||
## Operator Installation
|
||||
|
||||
@@ -152,6 +170,76 @@ _This mode of operation is ideal for continuous monitoring of your cluster and c
|
||||
- And use `k8sgpt analyze --explain` to get a more detailed explanation of the issues.
|
||||
- You also run `k8sgpt analyze --with-doc` (with or without the explain flag) to get the official documentation from Kubernetes.
|
||||
|
||||
# Using with Claude Desktop
|
||||
|
||||
K8sGPT can be integrated with Claude Desktop to provide AI-powered Kubernetes cluster analysis. This integration requires K8sGPT v0.4.14 or later.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install K8sGPT v0.4.14 or later:
|
||||
```sh
|
||||
brew install k8sgpt
|
||||
```
|
||||
|
||||
2. Install Claude Desktop from the official website
|
||||
|
||||
3. Configure K8sGPT with your preferred AI backend:
|
||||
```sh
|
||||
k8sgpt auth
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
1. Start the K8sGPT MCP server:
|
||||
```sh
|
||||
k8sgpt serve --mcp
|
||||
```
|
||||
|
||||
2. In Claude Desktop:
|
||||
- Open Settings
|
||||
- Navigate to the Integrations section
|
||||
- Add K8sGPT as a new integration
|
||||
- The MCP server will be automatically detected
|
||||
|
||||
3. Configure Claude Desktop with the following JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"k8sgpt": {
|
||||
"command": "k8sgpt",
|
||||
"args": [
|
||||
"serve",
|
||||
"--mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once connected, you can use Claude Desktop to:
|
||||
- Analyze your Kubernetes cluster
|
||||
- Get detailed insights about cluster health
|
||||
- Receive recommendations for fixing issues
|
||||
- Query cluster information
|
||||
|
||||
Example commands in Claude Desktop:
|
||||
- "Analyze my Kubernetes cluster"
|
||||
- "What's the health status of my cluster?"
|
||||
- "Show me any issues in the default namespace"
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter connection issues:
|
||||
1. Ensure K8sGPT is running with the MCP server enabled
|
||||
2. Verify your Kubernetes cluster is accessible
|
||||
3. Check that your AI backend is properly configured
|
||||
4. Restart both K8sGPT and Claude Desktop
|
||||
|
||||
For more information, visit our [documentation](https://docs.k8sgpt.ai).
|
||||
|
||||
## Analyzers
|
||||
|
||||
K8sGPT uses analyzers to triage and diagnose issues in your cluster. It has a set of analyzers that are built in, but
|
||||
@@ -169,10 +257,12 @@ you will be able to write your own analyzers.
|
||||
- [x] ingressAnalyzer
|
||||
- [x] statefulSetAnalyzer
|
||||
- [x] deploymentAnalyzer
|
||||
- [x] jobAnalyzer
|
||||
- [x] cronJobAnalyzer
|
||||
- [x] nodeAnalyzer
|
||||
- [x] mutatingWebhookAnalyzer
|
||||
- [x] validatingWebhookAnalyzer
|
||||
- [x] configMapAnalyzer
|
||||
|
||||
#### Optional
|
||||
|
||||
@@ -183,6 +273,16 @@ you will be able to write your own analyzers.
|
||||
- [x] gateway
|
||||
- [x] httproute
|
||||
- [x] logAnalyzer
|
||||
- [x] storageAnalyzer
|
||||
- [x] securityAnalyzer
|
||||
- [x] CatalogSource
|
||||
- [x] ClusterCatalog
|
||||
- [x] ClusterExtension
|
||||
- [x] ClusterService
|
||||
- [x] ClusterServiceVersion
|
||||
- [x] OperatorGroup
|
||||
- [x] InstallPlan
|
||||
- [x] Subscription
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -304,6 +404,26 @@ _Serve mode_
|
||||
k8sgpt serve
|
||||
```
|
||||
|
||||
_Serve mode with MCP (Model Context Protocol)_
|
||||
|
||||
```
|
||||
# Enable MCP server on default port 8089
|
||||
k8sgpt serve --mcp --mcp-http
|
||||
|
||||
# Enable MCP server on custom port
|
||||
k8sgpt serve --mcp --mcp-http --mcp-port 8089
|
||||
|
||||
# Full serve mode with MCP
|
||||
k8sgpt serve --mcp --mcp-http --port 8080 --metrics-port 8081 --mcp-port 8089
|
||||
```
|
||||
|
||||
The MCP server enables integration with tools like Claude Desktop and other MCP-compatible clients. It runs on port 8089 by default and provides:
|
||||
- Kubernetes cluster analysis via MCP protocol
|
||||
- Resource information and health status
|
||||
- AI-powered issue explanations and recommendations
|
||||
|
||||
For Helm chart deployment with MCP support, see the `charts/k8sgpt/values-mcp-example.yaml` file.
|
||||
|
||||
_Analysis with serve mode_
|
||||
|
||||
```
|
||||
@@ -324,17 +444,24 @@ _Print analysis stats_
|
||||
```
|
||||
k8sgpt analyze -s
|
||||
The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.
|
||||
- Analyzer Ingress took 47.125583ms
|
||||
- Analyzer PersistentVolumeClaim took 53.009167ms
|
||||
- Analyzer CronJob took 57.517792ms
|
||||
- Analyzer Deployment took 156.6205ms
|
||||
- Analyzer Node took 160.109833ms
|
||||
- Analyzer ReplicaSet took 245.938333ms
|
||||
- Analyzer StatefulSet took 448.0455ms
|
||||
- Analyzer Pod took 5.662594708s
|
||||
- Analyzer Ingress took 47.125583ms
|
||||
- Analyzer PersistentVolumeClaim took 53.009167ms
|
||||
- Analyzer CronJob took 57.517792ms
|
||||
- Analyzer Deployment took 156.6205ms
|
||||
- Analyzer Node took 160.109833ms
|
||||
- Analyzer ReplicaSet took 245.938333ms
|
||||
- Analyzer StatefulSet took 448.0455ms
|
||||
- Analyzer Pod took 5.662594708s
|
||||
- Analyzer Service took 38.583359166s
|
||||
```
|
||||
|
||||
_Diagnostic information_
|
||||
|
||||
To collect diagnostic information use the following command to create a `dump_<timestamp>_json` in your local directory.
|
||||
```
|
||||
k8sgpt dump
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## LLM AI Backends
|
||||
@@ -359,6 +486,8 @@ Unused:
|
||||
> huggingface
|
||||
> noopai
|
||||
> googlevertexai
|
||||
> watsonxai
|
||||
> customrest
|
||||
> ibmwatsonxai
|
||||
```
|
||||
|
||||
@@ -371,6 +500,22 @@ k8sgpt auth default -p azureopenai
|
||||
Default provider set to azureopenai
|
||||
```
|
||||
|
||||
_Using Amazon Bedrock with inference profiles_
|
||||
|
||||
_System Inference Profile_
|
||||
|
||||
```
|
||||
k8sgpt auth add --backend amazonbedrock --providerRegion us-east-1 --model arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-inference-profile
|
||||
|
||||
```
|
||||
|
||||
_Application Inference Profile_
|
||||
|
||||
```
|
||||
k8sgpt auth add --backend amazonbedrock --providerRegion us-east-1 --model arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/2uzp4s0w39t6
|
||||
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
<details>
|
||||
@@ -403,11 +548,9 @@ 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.**
|
||||
Note: **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**._
|
||||
|
||||
@@ -425,7 +568,7 @@ _In a few analysers like Pod, we feed to the AI backend the event messages which
|
||||
|
||||
- The following is the list of analysers in which data is **not being masked**:-
|
||||
|
||||
- RepicaSet
|
||||
- ReplicaSet
|
||||
- PersistentVolumeClaim
|
||||
- Pod
|
||||
- Log
|
||||
@@ -476,7 +619,7 @@ 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>
|
||||
<em>Note: You can only configure and use only one remote cache at a time</em>
|
||||
<em>Note: You can configure and use only one remote cache at a time</em>
|
||||
|
||||
_Adding a remote cache_
|
||||
|
||||
@@ -522,7 +665,7 @@ k8sgpt cache remove
|
||||
<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.
|
||||
K8sGPT now supports the ability to do so by abiding by the [schema](https://github.com/k8sgpt-ai/schemas/blob/main/protobuf/schema/v1/custom_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:
|
||||
|
||||
@@ -561,7 +704,30 @@ k8sgpt custom-analyzer remove --names "my-custom-analyzer,my-custom-analyzer-2"
|
||||
```
|
||||
|
||||
</details>
|
||||
## Model Context Protocol (MCP)
|
||||
|
||||
K8sGPT provides a Model Context Protocol server that exposes Kubernetes operations as standardized tools for AI assistants like Claude, ChatGPT, and other MCP-compatible clients.
|
||||
|
||||
**Start the MCP server:**
|
||||
|
||||
Stdio mode (for local AI assistants):
|
||||
```bash
|
||||
k8sgpt serve --mcp
|
||||
```
|
||||
|
||||
HTTP mode (for network access):
|
||||
```bash
|
||||
k8sgpt serve --mcp --mcp-http --mcp-port 8089
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- 12 tools for cluster analysis, resource management, and debugging
|
||||
- 3 resources for cluster information access
|
||||
- 3 interactive troubleshooting prompts
|
||||
- Stateless HTTP mode for one-off invocations
|
||||
- Full integration with Claude Desktop and other MCP clients
|
||||
|
||||
**Learn more:** See [MCP.md](MCP.md) for complete documentation, usage examples, and integration guides.
|
||||
## Documentation
|
||||
|
||||
Find our official documentation available [here](https://docs.k8sgpt.ai)
|
||||
@@ -572,7 +738,7 @@ Please read our [contributing guide](./CONTRIBUTING.md).
|
||||
|
||||
## Community
|
||||
|
||||
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-276pa9uyq-pxAUr4TCVHubFxEvLZuT1Q)
|
||||
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-332vhyaxv-bfjJwHZLXWVCB3QaXafEYQ)
|
||||
|
||||
<a href="https://github.com/k8sgpt-ai/k8sgpt/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=k8sgpt-ai/k8sgpt" />
|
||||
|
||||
83
SUPPORTED_MODELS.md
Normal file
83
SUPPORTED_MODELS.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Supported AI Providers and Models in K8sGPT
|
||||
|
||||
K8sGPT supports a variety of AI/LLM providers (backends). Some providers have a fixed set of supported models, while others allow you to specify any model supported by the provider.
|
||||
|
||||
---
|
||||
|
||||
## Providers and Supported Models
|
||||
|
||||
### OpenAI
|
||||
- **Model:** User-configurable (any model supported by OpenAI, e.g., `gpt-3.5-turbo`, `gpt-4`, etc.)
|
||||
|
||||
### Azure OpenAI
|
||||
- **Model:** User-configurable (any model deployed in your Azure OpenAI resource)
|
||||
|
||||
### LocalAI
|
||||
- **Model:** User-configurable (default: `llama3`)
|
||||
|
||||
### Ollama
|
||||
- **Model:** User-configurable (default: `llama3`, others can be specified)
|
||||
|
||||
### NoOpAI
|
||||
- **Model:** N/A (no real model, used for testing)
|
||||
|
||||
### Cohere
|
||||
- **Model:** User-configurable (any model supported by Cohere)
|
||||
|
||||
### Amazon Bedrock
|
||||
- **Supported Models:**
|
||||
- anthropic.claude-sonnet-4-20250514-v1:0
|
||||
- us.anthropic.claude-sonnet-4-20250514-v1:0
|
||||
- eu.anthropic.claude-sonnet-4-20250514-v1:0
|
||||
- apac.anthropic.claude-sonnet-4-20250514-v1:0
|
||||
- us.anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
- eu.anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
- apac.anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
- anthropic.claude-3-5-sonnet-20240620-v1:0
|
||||
- us.anthropic.claude-3-5-sonnet-20241022-v2:0
|
||||
- anthropic.claude-v2
|
||||
- anthropic.claude-v1
|
||||
- anthropic.claude-instant-v1
|
||||
- ai21.j2-ultra-v1
|
||||
- ai21.j2-jumbo-instruct
|
||||
- amazon.titan-text-express-v1
|
||||
- amazon.nova-pro-v1:0
|
||||
- eu.amazon.nova-pro-v1:0
|
||||
- us.amazon.nova-pro-v1:0
|
||||
- amazon.nova-lite-v1:0
|
||||
- eu.amazon.nova-lite-v1:0
|
||||
- us.amazon.nova-lite-v1:0
|
||||
- anthropic.claude-3-haiku-20240307-v1:0
|
||||
|
||||
> **Note:**
|
||||
> If you use an AWS Bedrock inference profile ARN (e.g., `arn:aws:bedrock:us-east-1:<account>:application-inference-profile/<id>`) as the model, you must still provide a valid modelId (e.g., `anthropic.claude-3-sonnet-20240229-v1:0`). K8sGPT will automatically set the required `X-Amzn-Bedrock-Inference-Profile-ARN` header for you when making requests to Bedrock.
|
||||
|
||||
### Amazon SageMaker
|
||||
- **Model:** User-configurable (any model deployed in your SageMaker endpoint)
|
||||
|
||||
### Google GenAI
|
||||
- **Model:** User-configurable (any model supported by Google GenAI, e.g., `gemini-pro`)
|
||||
|
||||
### Huggingface
|
||||
- **Model:** User-configurable (any model supported by Huggingface Inference API)
|
||||
|
||||
### Google VertexAI
|
||||
- **Supported Models:**
|
||||
- gemini-1.0-pro-001
|
||||
|
||||
### OCI GenAI
|
||||
- **Model:** User-configurable (any model supported by OCI GenAI)
|
||||
|
||||
### Custom REST
|
||||
- **Model:** User-configurable (any model your custom REST endpoint supports)
|
||||
|
||||
### IBM Watsonx
|
||||
- **Supported Models:**
|
||||
- ibm/granite-13b-chat-v2
|
||||
|
||||
### Groq
|
||||
- **Model:** User-configurable (any model supported by Groq, e.g., `llama-3.3-70b-versatile`, `mixtral-8x7b-32768`)
|
||||
|
||||
---
|
||||
|
||||
For more details on configuring each provider and model, refer to the official K8sGPT documentation and the provider's own documentation.
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: v0.3.0 #x-release-please-version
|
||||
appVersion: v0.4.23 #x-release-please-version
|
||||
description: A Helm chart for K8SGPT
|
||||
name: k8sgpt
|
||||
type: application
|
||||
|
||||
@@ -32,7 +32,13 @@ spec:
|
||||
image: {{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default .Chart.AppVersion }}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
args: ["serve"]
|
||||
{{- if .Values.deployment.mcp.enabled }}
|
||||
- containerPort: {{ .Values.deployment.mcp.port | int }}
|
||||
{{- end }}
|
||||
args: ["serve"
|
||||
{{- if .Values.deployment.mcp.enabled }}, "--mcp", "-v","--mcp-http", "--mcp-port", {{ .Values.deployment.mcp.port | quote }}
|
||||
{{- end }}
|
||||
]
|
||||
{{- if .Values.deployment.resources }}
|
||||
resources:
|
||||
{{- toYaml .Values.deployment.resources | nindent 10 }}
|
||||
|
||||
@@ -19,4 +19,9 @@ spec:
|
||||
- name: metrics
|
||||
port: 8081
|
||||
targetPort: 8081
|
||||
{{- if .Values.deployment.mcp.enabled }}
|
||||
- name: mcp
|
||||
port: {{ .Values.deployment.mcp.port | int }}
|
||||
targetPort: {{ .Values.deployment.mcp.port | int }}
|
||||
{{- end }}
|
||||
type: {{ .Values.service.type }}
|
||||
|
||||
39
charts/k8sgpt/values-mcp-example.yaml
Normal file
39
charts/k8sgpt/values-mcp-example.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
# Example values file to enable MCP (Model Context Protocol) service
|
||||
# Copy this file and modify as needed, then use: helm install -f values-mcp-example.yaml
|
||||
|
||||
deployment:
|
||||
# Enable MCP server
|
||||
mcp:
|
||||
enabled: true
|
||||
port: "8089" # Port for MCP server (default: 8089)
|
||||
http: true # Enable HTTP mode for MCP server
|
||||
|
||||
# Other deployment settings remain the same
|
||||
image:
|
||||
repository: ghcr.io/k8sgpt-ai/k8sgpt
|
||||
tag: "" # defaults to Chart.appVersion if unspecified
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
model: "gpt-3.5-turbo"
|
||||
backend: "openai"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: "512Mi"
|
||||
requests:
|
||||
cpu: "0.2"
|
||||
memory: "156Mi"
|
||||
|
||||
# Service configuration
|
||||
service:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
|
||||
# Secret configuration for AI backend
|
||||
secret:
|
||||
secretKey: "" # base64 encoded OpenAI token
|
||||
|
||||
# ServiceMonitor for Prometheus metrics
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
additionalLabels: {}
|
||||
@@ -7,6 +7,11 @@ deployment:
|
||||
env:
|
||||
model: "gpt-3.5-turbo"
|
||||
backend: "openai" # one of: [ openai | llama ]
|
||||
# MCP (Model Context Protocol) server configuration
|
||||
mcp:
|
||||
enabled: false # Enable MCP server
|
||||
port: "8089" # Port for MCP server
|
||||
http: true # Enable HTTP mode for MCP server
|
||||
resources:
|
||||
limits:
|
||||
cpu: "1"
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/interactive"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -67,25 +68,45 @@ var AnalyzeCmd = &cobra.Command{
|
||||
withStats,
|
||||
)
|
||||
|
||||
verbose := viper.GetBool("verbose")
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking analysis configuration.")
|
||||
}
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Println("Debug: Analysis initialized.")
|
||||
}
|
||||
defer config.Close()
|
||||
|
||||
if customAnalysis {
|
||||
config.RunCustomAnalysis()
|
||||
if verbose {
|
||||
fmt.Println("Debug: All custom analyzers completed.")
|
||||
}
|
||||
}
|
||||
config.RunAnalysis()
|
||||
if verbose {
|
||||
fmt.Println("Debug: All core analyzers completed.")
|
||||
}
|
||||
|
||||
if explain {
|
||||
if err := config.GetAIResults(output, anonymize); err != nil {
|
||||
err := config.GetAIResults(output, anonymize)
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking AI results.")
|
||||
}
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
// print results
|
||||
output_data, err := config.PrintOutput(output)
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking output.")
|
||||
}
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
|
||||
const (
|
||||
defaultBackend = "openai"
|
||||
defaultModel = "gpt-3.5-turbo"
|
||||
defaultModel = "gpt-4o"
|
||||
)
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
|
||||
@@ -90,7 +90,7 @@ var updateCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
if !foundBackend {
|
||||
color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", args[0])
|
||||
color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", backend)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
||||
10
cmd/cache/add.go
vendored
10
cmd/cache/add.go
vendored
@@ -17,6 +17,7 @@ package cache
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
@@ -40,9 +41,10 @@ var addCmd = &cobra.Command{
|
||||
Short: "Add a remote cache",
|
||||
Long: `This command allows you to add a remote cache to store the results of an analysis.
|
||||
The supported cache types are:
|
||||
- Azure Blob storage
|
||||
- Google Cloud storage
|
||||
- S3`,
|
||||
- Azure Blob storage (e.g., k8sgpt cache add azure)
|
||||
- Google Cloud storage (e.g., k8sgpt cache add gcs)
|
||||
- S3 (e.g., k8sgpt cache add s3)
|
||||
- Interplex (e.g., k8sgpt cache add interplex)`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
color.Red("Error: Please provide a value for cache types. Run k8sgpt cache add --help")
|
||||
@@ -50,7 +52,7 @@ var addCmd = &cobra.Command{
|
||||
}
|
||||
fmt.Println(color.YellowString("Adding remote based cache"))
|
||||
cacheType := args[0]
|
||||
remoteCache, err := cache.NewCacheProvider(cacheType, bucketName, region, endpoint, storageAccount, containerName, projectId, insecure)
|
||||
remoteCache, err := cache.NewCacheProvider(strings.ToLower(cacheType), bucketName, region, endpoint, storageAccount, containerName, projectId, insecure)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
45
cmd/cache/get.go
vendored
Normal file
45
cmd/cache/get.go
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
)
|
||||
|
||||
// listCmd represents the list command
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get the current cache",
|
||||
Long: `Returns the current remote cache being used`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// load remote cache if it is configured
|
||||
c, err := cache.GetCacheConfiguration()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Current remote cache is: %s", c.GetName())
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
CacheCmd.AddCommand(getCmd)
|
||||
|
||||
}
|
||||
43
cmd/cache/purge.go
vendored
43
cmd/cache/purge.go
vendored
@@ -23,23 +23,51 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var all bool
|
||||
|
||||
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",
|
||||
Long: "This command allows you to delete/purge one object from the cache or all objects with --all flag.",
|
||||
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)
|
||||
}
|
||||
|
||||
if all {
|
||||
fmt.Println(color.YellowString("Purging all objects from the remote cache."))
|
||||
names, err := c.List()
|
||||
if err != nil {
|
||||
color.Red("Error listing cache objects: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
fmt.Println(color.GreenString("No objects to delete."))
|
||||
return
|
||||
}
|
||||
var failed []string
|
||||
for _, obj := range names {
|
||||
err := c.Remove(obj.Name)
|
||||
if err != nil {
|
||||
failed = append(failed, obj.Name)
|
||||
}
|
||||
}
|
||||
if len(failed) > 0 {
|
||||
color.Red("Failed to delete: %v", failed)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(color.GreenString("All objects deleted."))
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
color.Red("Error: Please provide a value for object name or use --all. Run k8sgpt cache purge --help")
|
||||
os.Exit(1)
|
||||
}
|
||||
objectKey := args[0]
|
||||
fmt.Println(color.YellowString("Purging a remote cache."))
|
||||
err = c.Remove(objectKey)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
@@ -50,5 +78,6 @@ var purgeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
purgeCmd.Flags().BoolVar(&all, "all", false, "Purge all objects in the cache")
|
||||
CacheCmd.AddCommand(purgeCmd)
|
||||
}
|
||||
|
||||
113
cmd/dump/dump.go
Normal file
113
cmd/dump/dump.go
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package dump
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/version"
|
||||
)
|
||||
|
||||
type K8sGPTInfo struct {
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
}
|
||||
type DumpOut struct {
|
||||
AIConfiguration ai.AIConfiguration
|
||||
ActiveFilters []string
|
||||
KubenetesServerVersion *version.Info
|
||||
K8sGPTInfo K8sGPTInfo
|
||||
}
|
||||
|
||||
var DumpCmd = &cobra.Command{
|
||||
Use: "dump",
|
||||
Short: "Creates a dumpfile for debugging issues with K8sGPT",
|
||||
Long: `The dump command will create a dump.*.json which will contain K8sGPT non-sensitive configuration information.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// Fetch the configuration object(s)
|
||||
// get ai configuration
|
||||
var configAI ai.AIConfiguration
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var newProvider []ai.AIProvider
|
||||
for _, config := range configAI.Providers {
|
||||
// we blank out the custom headers for data protection reasons
|
||||
config.CustomHeaders = make([]http.Header, 0)
|
||||
// blank out the password
|
||||
if len(config.Password) > 4 {
|
||||
config.Password = config.Password[:4] + "***"
|
||||
} else {
|
||||
// If the password is shorter than 4 characters
|
||||
config.Password = "***"
|
||||
}
|
||||
newProvider = append(newProvider, config)
|
||||
}
|
||||
configAI.Providers = newProvider
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
kubecontext := viper.GetString("kubecontext")
|
||||
kubeconfig := viper.GetString("kubeconfig")
|
||||
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
v, err := client.Client.Discovery().ServerVersion()
|
||||
if err != nil {
|
||||
color.Yellow("Could not find kubernetes server version")
|
||||
}
|
||||
var dumpOut DumpOut = DumpOut{
|
||||
AIConfiguration: configAI,
|
||||
ActiveFilters: activeFilters,
|
||||
KubenetesServerVersion: v,
|
||||
K8sGPTInfo: K8sGPTInfo{
|
||||
Version: viper.GetString("Version"),
|
||||
Commit: viper.GetString("Commit"),
|
||||
Date: viper.GetString("Date"),
|
||||
},
|
||||
}
|
||||
// Serialize dumpOut to JSON
|
||||
jsonData, err := json.MarshalIndent(dumpOut, "", " ")
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Write JSON data to file
|
||||
f := fmt.Sprintf("dump_%s.json", time.Now().Format("20060102150405"))
|
||||
err = os.WriteFile(f, jsonData, 0644)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
color.Green("Dump created successfully: %s", f)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
}
|
||||
@@ -45,7 +45,7 @@ var GenerateCmd = &cobra.Command{
|
||||
backendType = backend
|
||||
}
|
||||
fmt.Println("")
|
||||
openbrowser("https://beta.openai.com/account/api-keys")
|
||||
openbrowser("https://platform.openai.com/api-keys")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -81,10 +81,10 @@ func openbrowser(url string) {
|
||||
func printInstructions(isGui bool, backendType string) {
|
||||
fmt.Println("")
|
||||
if isGui {
|
||||
color.Green("Opening: https://beta.openai.com/account/api-keys to generate a key for %s", backendType)
|
||||
color.Green("Opening: https://platform.openai.com/api-keys to generate a key for %s", backendType)
|
||||
fmt.Println("")
|
||||
} else {
|
||||
color.Green("Please open: https://beta.openai.com/account/api-keys to generate a key for %s", backendType)
|
||||
color.Green("Please open: https://platform.openai.com/api-keys to generate a key for %s", backendType)
|
||||
fmt.Println("")
|
||||
}
|
||||
color.Green("Please copy the generated key and run `k8sgpt auth add` to add it to your config file")
|
||||
|
||||
@@ -24,7 +24,7 @@ var deactivateCmd = &cobra.Command{
|
||||
Use: "deactivate [integration]",
|
||||
Short: "Deactivate an integration",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Long: `For example e.g. k8sgpt integration deactivate trivy`,
|
||||
Long: `For example e.g. k8sgpt integration deactivate prometheus`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
integrationName := args[0]
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@ var IntegrationCmd = &cobra.Command{
|
||||
Short: "Integrate another tool into K8sGPT",
|
||||
Long: `Integrate another tool into K8sGPT. For example:
|
||||
|
||||
k8sgpt integration activate trivy
|
||||
k8sgpt integration activate prometheus
|
||||
|
||||
This would allow you to deploy trivy into your cluster and use a K8sGPT analyzer to parse trivy results.`,
|
||||
This would allow you to connect to prometheus running with your cluster.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
_ = cmd.Help()
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/auth"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/cache"
|
||||
customanalyzer "github.com/k8sgpt-ai/k8sgpt/cmd/customAnalyzer"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/dump"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/filters"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/generate"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/integration"
|
||||
@@ -36,6 +37,7 @@ var (
|
||||
cfgFile string
|
||||
kubecontext string
|
||||
kubeconfig string
|
||||
verbose bool
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
@@ -57,6 +59,9 @@ func Execute(v string, c string, d string) {
|
||||
Version = v
|
||||
Commit = c
|
||||
Date = d
|
||||
viper.Set("Version", Version)
|
||||
viper.Set("Commit", Commit)
|
||||
viper.Set("Date", Date)
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
@@ -70,6 +75,7 @@ func init() {
|
||||
|
||||
rootCmd.AddCommand(auth.AuthCmd)
|
||||
rootCmd.AddCommand(analyze.AnalyzeCmd)
|
||||
rootCmd.AddCommand(dump.DumpCmd)
|
||||
rootCmd.AddCommand(filters.FiltersCmd)
|
||||
rootCmd.AddCommand(generate.GenerateCmd)
|
||||
rootCmd.AddCommand(integration.IntegrationCmd)
|
||||
@@ -79,6 +85,7 @@ func init() {
|
||||
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.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show detailed tool actions (e.g., API calls, checks).")
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
@@ -99,6 +106,7 @@ func initConfig() {
|
||||
|
||||
viper.Set("kubecontext", kubecontext)
|
||||
viper.Set("kubeconfig", kubeconfig)
|
||||
viper.Set("verbose", verbose)
|
||||
|
||||
viper.SetEnvPrefix("K8SGPT")
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
30
cmd/root_test.go
Normal file
30
cmd/root_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Test that verbose flag is correctly set in viper.
|
||||
func TestInitConfig_VerboseFlag(t *testing.T) {
|
||||
verbose = true
|
||||
viper.Reset()
|
||||
initConfig()
|
||||
if !viper.GetBool("verbose") {
|
||||
t.Error("Expected verbose flag to be true")
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ const (
|
||||
defaultTemperature float32 = 0.7
|
||||
defaultTopP float32 = 1.0
|
||||
defaultTopK int32 = 50
|
||||
defaultMaxTokens int = 2048
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -37,6 +38,11 @@ var (
|
||||
metricsPort string
|
||||
backend string
|
||||
enableHttp bool
|
||||
enableMCP bool
|
||||
mcpPort string
|
||||
mcpHTTP bool
|
||||
// filters can be injected into the server (repeatable flag)
|
||||
filters []string
|
||||
)
|
||||
|
||||
var ServeCmd = &cobra.Command{
|
||||
@@ -102,6 +108,18 @@ var ServeCmd = &cobra.Command{
|
||||
}
|
||||
return int32(topK)
|
||||
}
|
||||
maxTokens := func() int {
|
||||
env := os.Getenv("K8SGPT_MAX_TOKENS")
|
||||
if env == "" {
|
||||
return defaultMaxTokens
|
||||
}
|
||||
maxTokens, err := strconv.ParseInt(env, 10, 32)
|
||||
if err != nil {
|
||||
color.Red("Unable to convert maxTokens value: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return int(maxTokens)
|
||||
}
|
||||
// Check for env injection
|
||||
backend = os.Getenv("K8SGPT_BACKEND")
|
||||
password := os.Getenv("K8SGPT_PASSWORD")
|
||||
@@ -125,6 +143,7 @@ var ServeCmd = &cobra.Command{
|
||||
Temperature: temperature(),
|
||||
TopP: topP(),
|
||||
TopK: topK(),
|
||||
MaxTokens: maxTokens(),
|
||||
}
|
||||
|
||||
configAI.Providers = append(configAI.Providers, *aiProvider)
|
||||
@@ -169,6 +188,26 @@ var ServeCmd = &cobra.Command{
|
||||
}
|
||||
}()
|
||||
|
||||
if enableMCP {
|
||||
// Create and start MCP server
|
||||
mcpServer, err := k8sgptserver.NewMCPServer(mcpPort, aiProvider, mcpHTTP, logger)
|
||||
if err != nil {
|
||||
color.Red("Error creating MCP server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
go func() {
|
||||
if err := mcpServer.Start(); err != nil {
|
||||
color.Red("Error starting MCP server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Allow metrics port to be overridden by environment variable
|
||||
if envMetricsPort := os.Getenv("K8SGPT_METRICS_PORT"); envMetricsPort != "" && !cmd.Flags().Changed("metrics-port") {
|
||||
metricsPort = envMetricsPort
|
||||
}
|
||||
|
||||
server := k8sgptserver.Config{
|
||||
Backend: aiProvider.Name,
|
||||
Port: port,
|
||||
@@ -176,6 +215,7 @@ var ServeCmd = &cobra.Command{
|
||||
EnableHttp: enableHttp,
|
||||
Token: aiProvider.Password,
|
||||
Logger: logger,
|
||||
Filters: filters,
|
||||
}
|
||||
go func() {
|
||||
if err := server.ServeMetrics(); err != nil {
|
||||
@@ -199,7 +239,12 @@ var ServeCmd = &cobra.Command{
|
||||
func init() {
|
||||
// add flag for backend
|
||||
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(&metricsPort, "metrics-port", "m", "8081", "Port to run the metrics-server on (env: K8SGPT_METRICS_PORT)")
|
||||
ServeCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
|
||||
ServeCmd.Flags().BoolVarP(&enableHttp, "http", "", false, "Enable REST/http using gppc-gateway")
|
||||
ServeCmd.Flags().BoolVarP(&enableMCP, "mcp", "", false, "Enable Mission Control Protocol server")
|
||||
ServeCmd.Flags().StringVarP(&mcpPort, "mcp-port", "", "8089", "Port to run the MCP server on")
|
||||
ServeCmd.Flags().BoolVarP(&mcpHTTP, "mcp-http", "", false, "Enable HTTP mode for MCP server")
|
||||
// allow injecting filters into the running server (repeatable)
|
||||
ServeCmd.Flags().StringSliceVar(&filters, "filter", []string{}, "Filter to apply (can be specified multiple times)")
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM golang:1.23-alpine3.19 AS builder
|
||||
FROM golang:1.24-alpine3.23 AS builder
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
ARG VERSION
|
||||
|
||||
307
go.mod
307
go.mod
@@ -1,197 +1,165 @@
|
||||
module github.com/k8sgpt-ai/k8sgpt
|
||||
|
||||
go 1.23.3
|
||||
go 1.24.1
|
||||
|
||||
toolchain go1.24.11
|
||||
|
||||
require (
|
||||
github.com/aquasecurity/trivy-operator v0.22.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/kedacore/keda/v2 v2.16.0
|
||||
github.com/magiconair/properties v1.8.7
|
||||
github.com/magiconair/properties v1.8.9
|
||||
github.com/mittwald/go-helm-client v0.12.14
|
||||
github.com/ollama/ollama v0.4.1
|
||||
github.com/sashabaranov/go-openai v1.35.6
|
||||
github.com/ollama/ollama v0.13.4
|
||||
github.com/sashabaranov/go-openai v1.36.0
|
||||
github.com/schollz/progressbar/v3 v3.17.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/term v0.26.0
|
||||
helm.sh/helm/v3 v3.16.2
|
||||
k8s.io/api v0.31.2
|
||||
k8s.io/apimachinery v0.31.2
|
||||
k8s.io/client-go v0.31.2
|
||||
k8s.io/kubectl v0.31.1 // indirect
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/term v0.33.0
|
||||
helm.sh/helm/v3 v3.17.4
|
||||
k8s.io/api v0.32.3
|
||||
k8s.io/apimachinery v0.32.3
|
||||
k8s.io/client-go v0.32.3
|
||||
k8s.io/kubectl v0.32.2 // indirect
|
||||
|
||||
)
|
||||
|
||||
require github.com/adrg/xdg v0.5.3
|
||||
|
||||
require (
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.23.0-20240920204244-7a91c8620515.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20240920204244-7a91c8620515.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.1-20240920204244-7a91c8620515.1
|
||||
cloud.google.com/go/storage v1.46.0
|
||||
buf.build/gen/go/interplex-ai/schemas/grpc/go v1.5.1-20241117203254-a91193b62179.1
|
||||
buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go v1.35.2-20241117203254-a91193b62179.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.24.0-20241118152629-1379a5a1889d.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1
|
||||
cloud.google.com/go/storage v1.50.0
|
||||
cloud.google.com/go/vertexai v0.13.2
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0
|
||||
github.com/IBM/watsonx-go v1.0.1
|
||||
github.com/aws/aws-sdk-go v1.55.5
|
||||
github.com/cohere-ai/cohere-go/v2 v2.12.0
|
||||
github.com/agiledragon/gomonkey/v2 v2.13.0
|
||||
github.com/aws/aws-sdk-go v1.55.7
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrock v1.33.0
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0
|
||||
github.com/aws/smithy-go v1.22.2
|
||||
github.com/cohere-ai/cohere-go/v2 v2.12.2
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/generative-ai-go v0.18.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0
|
||||
github.com/google/generative-ai-go v0.19.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
|
||||
github.com/hupe1980/go-huggingface v0.0.15
|
||||
github.com/kyverno/policy-reporter-kyverno-plugin v1.6.4
|
||||
github.com/mark3labs/mcp-go v0.36.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/oracle/oci-go-sdk/v65 v65.78.0
|
||||
github.com/prometheus/prometheus v0.55.1
|
||||
github.com/pterm/pterm v0.12.79
|
||||
google.golang.org/api v0.205.0
|
||||
github.com/oracle/oci-go-sdk/v65 v65.79.0
|
||||
github.com/prometheus/prometheus v0.306.0
|
||||
github.com/pterm/pterm v0.12.80
|
||||
google.golang.org/api v0.239.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
sigs.k8s.io/controller-runtime v0.19.1
|
||||
sigs.k8s.io/gateway-api v1.2.0
|
||||
sigs.k8s.io/controller-runtime v0.19.3
|
||||
sigs.k8s.io/gateway-api v1.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
atomicgo.dev/cursor v0.2.0 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
cel.dev/expr v0.16.1 // indirect
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cel.dev/expr v0.23.0 // indirect
|
||||
cloud.google.com/go v0.120.0 // indirect
|
||||
cloud.google.com/go/ai v0.8.0 // indirect
|
||||
cloud.google.com/go/aiplatform v1.68.0 // indirect
|
||||
cloud.google.com/go/auth v0.10.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.2 // indirect
|
||||
cloud.google.com/go/iam v1.2.1 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.1 // indirect
|
||||
cloud.google.com/go/monitoring v1.21.2 // indirect
|
||||
cloud.google.com/go/aiplatform v1.85.0 // indirect
|
||||
cloud.google.com/go/auth v0.16.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.3 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect
|
||||
github.com/Microsoft/hcsshim v0.12.4 // indirect
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect
|
||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/aquasecurity/go-version v0.0.0-20240603093900-cf8a8d29271d // indirect
|
||||
github.com/aquasecurity/trivy-checks v0.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.55.1 // indirect
|
||||
github.com/aws/smithy-go v1.22.0 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
|
||||
github.com/containerd/console v1.0.4 // indirect
|
||||
github.com/containerd/errdefs v0.1.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.1.1 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/creack/pty v1.1.21 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.13.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/expr-lang/expr v1.16.9 // indirect
|
||||
github.com/expr-lang/expr v1.17.7 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.12.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gorilla/websocket v1.5.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-getter v1.7.5 // indirect
|
||||
github.com/hashicorp/go-safetemp v1.0.0 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.20.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/invopop/jsonschema v0.13.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/liamg/iamgo v0.0.9 // indirect
|
||||
github.com/liamg/jfather v0.0.7 // indirect
|
||||
github.com/liamg/memoryfs v1.6.0 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
|
||||
github.com/masahiro331/go-disk v0.0.0-20220919035250-c8da316f91ac // indirect
|
||||
github.com/masahiro331/go-ext4-filesystem v0.0.0-20231208112839-4339555a0cd4 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/moby/buildkit v0.13.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.1 // indirect
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/open-policy-agent/opa v0.65.0 // indirect
|
||||
github.com/owenrumney/squealer v1.2.2 // indirect
|
||||
github.com/package-url/packageurl-go v0.1.3 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/prometheus/common/sigv4 v0.1.0 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/prometheus/sigv4 v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/segmentio/fasthash v1.0.3 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.2.2 // indirect
|
||||
github.com/sony/gobreaker v0.5.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yashtewari/glob-intersection v0.2.0 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.0.3 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
|
||||
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect
|
||||
mvdan.cc/sh/v3 v3.8.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -203,27 +171,23 @@ require (
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/aquasecurity/table v1.8.0 // indirect
|
||||
github.com/aquasecurity/tml v0.6.1 // indirect
|
||||
github.com/aquasecurity/trivy v0.53.0 // indirect
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20231020043206-3770774790ce // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.7.18 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.1 // indirect
|
||||
github.com/containerd/containerd v1.7.29 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v26.1.4+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v27.2.0+incompatible // indirect
|
||||
github.com/docker/docker v28.3.0+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
||||
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.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.4.2 // indirect
|
||||
@@ -236,8 +200,7 @@ require (
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/gnostic v0.7.0
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/go-containerregistry v0.19.2 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@@ -248,18 +211,16 @@ require (
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
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-20230608043311-a335f4599b70 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
@@ -269,7 +230,7 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/spdystream v0.4.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -281,58 +242,58 @@ require (
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.60.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/prometheus/client_golang v1.23.0-rc.1
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rubenv/sql-migrate v1.7.0 // indirect
|
||||
github.com/rubenv/sql-migrate v1.7.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/samber/lo v1.39.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
|
||||
golang.org/x/net v0.31.0
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/grpc v1.73.0
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.31.2
|
||||
k8s.io/apiserver v0.31.2 // indirect
|
||||
k8s.io/cli-runtime v0.31.1 // indirect
|
||||
k8s.io/component-base v0.31.2 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.32.2
|
||||
k8s.io/apiserver v0.32.2 // indirect
|
||||
k8s.io/cli-runtime v0.32.2 // indirect
|
||||
k8s.io/component-base v0.32.2 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 // indirect
|
||||
k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
|
||||
oras.land/oras-go v1.2.5 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.18.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
// v1.2.0 is taken from github.com/open-policy-agent/opa v0.42.0
|
||||
// v1.2.0 incompatible with github.com/docker/docker v23.0.0-rc.1+incompatible
|
||||
//replace oras.land/oras-go => oras.land/oras-go v1.2.4
|
||||
replace github.com/docker/docker => github.com/docker/docker v27.3.1+incompatible
|
||||
replace github.com/docker/docker => github.com/docker/docker v28.0.4+incompatible
|
||||
|
||||
replace dario.cat/mergo => github.com/imdario/mergo v1.0.1
|
||||
|
||||
@@ -2,13 +2,20 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/bedrockruntime"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/bedrock"
|
||||
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
|
||||
"github.com/aws/smithy-go/middleware"
|
||||
smithyhttp "github.com/aws/smithy-go/transport/http"
|
||||
)
|
||||
|
||||
const amazonbedrockAIClientName = "amazonbedrock"
|
||||
@@ -17,14 +24,16 @@ const amazonbedrockAIClientName = "amazonbedrock"
|
||||
type AmazonBedRockClient struct {
|
||||
nopCloser
|
||||
|
||||
client *bedrockruntime.BedrockRuntime
|
||||
model string
|
||||
client BedrockRuntimeAPI
|
||||
mgmtClient BedrockManagementAPI
|
||||
model *bedrock_support.BedrockModel
|
||||
temperature float32
|
||||
topP float32
|
||||
maxTokens int
|
||||
topP float32
|
||||
maxTokens int
|
||||
models []bedrock_support.BedrockModel
|
||||
}
|
||||
|
||||
// Amazon BedRock support region list US East (N. Virginia),US West (Oregon),Asia Pacific (Singapore),Asia Pacific (Tokyo),Europe (Frankfurt)
|
||||
// AmazonCompletion 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
|
||||
|
||||
@@ -34,6 +43,9 @@ const (
|
||||
AP_Southeast_1 = "ap-southeast-1"
|
||||
AP_Northeast_1 = "ap-northeast-1"
|
||||
EU_Central_1 = "eu-central-1"
|
||||
AP_South_1 = "ap-south-1"
|
||||
US_Gov_West_1 = "us-gov-west-1"
|
||||
US_Gov_East_1 = "us-gov-east-1"
|
||||
)
|
||||
|
||||
var BEDROCKER_SUPPORTED_REGION = []string{
|
||||
@@ -42,45 +54,294 @@ var BEDROCKER_SUPPORTED_REGION = []string{
|
||||
AP_Southeast_1,
|
||||
AP_Northeast_1,
|
||||
EU_Central_1,
|
||||
AP_South_1,
|
||||
US_Gov_West_1,
|
||||
US_Gov_East_1,
|
||||
}
|
||||
|
||||
const (
|
||||
ModelAnthropicClaudeV2 = "anthropic.claude-v2"
|
||||
ModelAnthropicClaudeV1 = "anthropic.claude-v1"
|
||||
ModelAnthropicClaudeInstantV1 = "anthropic.claude-instant-v1"
|
||||
ModelA21J2UltraV1 = "ai21.j2-ultra-v1"
|
||||
ModelA21J2JumboInstruct = "ai21.j2-jumbo-instruct"
|
||||
ModelAmazonTitanExpressV1 = "amazon.titan-text-express-v1"
|
||||
)
|
||||
var defaultModels = []bedrock_support.BedrockModel{
|
||||
|
||||
var BEDROCK_MODELS = []string{
|
||||
ModelAnthropicClaudeV2,
|
||||
ModelAnthropicClaudeV1,
|
||||
ModelAnthropicClaudeInstantV1,
|
||||
ModelA21J2UltraV1,
|
||||
ModelA21J2JumboInstruct,
|
||||
ModelAmazonTitanExpressV1,
|
||||
{
|
||||
Name: "anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "us.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "us.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "eu.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "eu.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "apac.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "apac.anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "eu.anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "eu.anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "apac.anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "apac.anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "anthropic.claude-v2",
|
||||
Completion: &bedrock_support.CohereCompletion{},
|
||||
Response: &bedrock_support.CohereResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-v2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "anthropic.claude-v1",
|
||||
Completion: &bedrock_support.CohereCompletion{},
|
||||
Response: &bedrock_support.CohereResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "anthropic.claude-instant-v1",
|
||||
Completion: &bedrock_support.CohereCompletion{},
|
||||
Response: &bedrock_support.CohereResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-instant-v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ai21.j2-ultra-v1",
|
||||
Completion: &bedrock_support.AI21{},
|
||||
Response: &bedrock_support.AI21Response{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "ai21.j2-ultra-v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ai21.j2-jumbo-instruct",
|
||||
Completion: &bedrock_support.AI21{},
|
||||
Response: &bedrock_support.AI21Response{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "ai21.j2-jumbo-instruct",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "amazon.titan-text-express-v1",
|
||||
Completion: &bedrock_support.AmazonCompletion{},
|
||||
Response: &bedrock_support.AmazonResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "amazon.titan-text-express-v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "amazon.nova-pro-v1:0",
|
||||
Completion: &bedrock_support.AmazonCompletion{},
|
||||
Response: &bedrock_support.NovaResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
// https://docs.aws.amazon.com/nova/latest/userguide/getting-started-api.html
|
||||
MaxTokens: 100, // max of 300k tokens
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "amazon.nova-pro-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "eu.amazon.nova-pro-v1:0",
|
||||
Completion: &bedrock_support.AmazonCompletion{},
|
||||
Response: &bedrock_support.NovaResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
// https://docs.aws.amazon.com/nova/latest/userguide/getting-started-api.html
|
||||
MaxTokens: 100, // max of 300k tokens
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "eu.amazon.nova-pro-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "us.amazon.nova-pro-v1:0",
|
||||
Completion: &bedrock_support.AmazonCompletion{},
|
||||
Response: &bedrock_support.NovaResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
// https://docs.aws.amazon.com/nova/latest/userguide/getting-started-api.html
|
||||
MaxTokens: 100, // max of 300k tokens
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "us.amazon.nova-pro-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "amazon.nova-lite-v1:0",
|
||||
Completion: &bedrock_support.AmazonCompletion{},
|
||||
Response: &bedrock_support.NovaResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100, // max of 300k tokens
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "amazon.nova-lite-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "eu.amazon.nova-lite-v1:0",
|
||||
Completion: &bedrock_support.AmazonCompletion{},
|
||||
Response: &bedrock_support.NovaResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100, // max of 300k tokens
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "eu.amazon.nova-lite-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "us.amazon.nova-lite-v1:0",
|
||||
Completion: &bedrock_support.AmazonCompletion{},
|
||||
Response: &bedrock_support.NovaResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100, // max of 300k tokens
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "us.amazon.nova-lite-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "anthropic.claude-3-haiku-20240307-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
// sensible defaults
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-3-haiku-20240307-v1:0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//const TOPP = 0.9 moved to config
|
||||
|
||||
// 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
|
||||
}
|
||||
// NewAmazonBedRockClient creates a new AmazonBedRockClient with the given models
|
||||
func NewAmazonBedRockClient(models []bedrock_support.BedrockModel) *AmazonBedRockClient {
|
||||
if models == nil {
|
||||
models = defaultModels // Use default models if none provided
|
||||
}
|
||||
return &AmazonBedRockClient{
|
||||
models: models,
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
@@ -95,23 +356,138 @@ func GetRegionOrDefault(region string) string {
|
||||
return BEDROCK_DEFAULT_REGION
|
||||
}
|
||||
|
||||
// Configure configures the AmazonBedRockClient with the provided configuration.
|
||||
func (a *AmazonBedRockClient) Configure(config IAIConfig) error {
|
||||
func validateModelArn(model string) bool {
|
||||
var re = regexp.MustCompile(`(?m)^arn:(?P<Partition>[^:\n]*):bedrock:(?P<Region>[^:\n]*):(?P<AccountID>[^:\n]*):(?P<Ignore>(?P<ResourceType>[^:\/\n]*)[:\/])?(?P<Resource>.*)$`)
|
||||
return re.MatchString(model)
|
||||
}
|
||||
|
||||
// Create a new AWS session
|
||||
providerRegion := GetRegionOrDefault(config.GetProviderRegion())
|
||||
func validateInferenceProfileArn(inferenceProfile string) bool {
|
||||
// Support both inference-profile and application-inference-profile formats
|
||||
var re = regexp.MustCompile(`(?m)^arn:(?P<Partition>[^:\n]*):bedrock:(?P<Region>[^:\n]*):(?P<AccountID>[^:\n]*):(?:inference-profile|application-inference-profile)\/(?P<ProfileName>.+)$`)
|
||||
return re.MatchString(inferenceProfile)
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(providerRegion),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
// Get model from string
|
||||
func (a *AmazonBedRockClient) getModelFromString(model string) (*bedrock_support.BedrockModel, error) {
|
||||
if model == "" {
|
||||
return nil, errors.New("model name cannot be empty")
|
||||
}
|
||||
|
||||
// Create a new BedrockRuntime client
|
||||
a.client = bedrockruntime.New(sess)
|
||||
a.model = GetModelOrDefault(config.GetModel())
|
||||
// Trim spaces from the model name
|
||||
model = strings.TrimSpace(model)
|
||||
|
||||
// Try to find an exact match first
|
||||
for i := range a.models {
|
||||
if strings.EqualFold(model, a.models[i].Name) || strings.EqualFold(model, a.models[i].Config.ModelName) {
|
||||
// Create a copy to avoid returning a pointer to a loop variable
|
||||
modelCopy := a.models[i]
|
||||
return &modelCopy, nil
|
||||
}
|
||||
}
|
||||
|
||||
supportedModels := make([]string, len(a.models))
|
||||
for i, m := range a.models {
|
||||
supportedModels[i] = m.Name
|
||||
}
|
||||
|
||||
supportedRegions := BEDROCKER_SUPPORTED_REGION
|
||||
|
||||
// Pretty-print supported models and regions
|
||||
modelList := ""
|
||||
for _, m := range supportedModels {
|
||||
modelList += " - " + m + "\n"
|
||||
}
|
||||
regionList := ""
|
||||
for _, r := range supportedRegions {
|
||||
regionList += " - " + r + "\n"
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf(
|
||||
"model '%s' not found in supported models.\n\nSupported models:\n%sSupported regions:\n%s",
|
||||
model, modelList, regionList,
|
||||
)
|
||||
}
|
||||
|
||||
// Configure configures the AmazonBedRockClient with the provided configuration.
|
||||
func (a *AmazonBedRockClient) Configure(config IAIConfig) error {
|
||||
// Initialize models if not already initialized
|
||||
if a.models == nil {
|
||||
a.models = defaultModels
|
||||
}
|
||||
|
||||
// Get the model input
|
||||
modelInput := config.GetModel()
|
||||
|
||||
// Determine the appropriate region to use
|
||||
var region string
|
||||
|
||||
// Check if the model input is actually an inference profile ARN
|
||||
if validateInferenceProfileArn(modelInput) {
|
||||
// Extract the region from the inference profile ARN
|
||||
arnParts := strings.Split(modelInput, ":")
|
||||
if len(arnParts) >= 4 {
|
||||
region = arnParts[3]
|
||||
} else {
|
||||
return fmt.Errorf("could not extract region from inference profile ARN: %s", modelInput)
|
||||
}
|
||||
} else {
|
||||
// Use the provided region or default
|
||||
region = GetRegionOrDefault(config.GetProviderRegion())
|
||||
}
|
||||
|
||||
// Only create AWS clients if they haven't been injected (for testing)
|
||||
if a.client == nil || a.mgmtClient == nil {
|
||||
// Create a new AWS config with the determined region
|
||||
cfg, err := awsconfig.LoadDefaultConfig(context.Background(),
|
||||
awsconfig.WithRegion(region),
|
||||
)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "SignatureDoesNotMatch") || strings.Contains(err.Error(), "NoCredentialProviders") {
|
||||
return fmt.Errorf("AWS credentials are invalid or missing. Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or AWS config. Details: %v", err)
|
||||
}
|
||||
return fmt.Errorf("failed to load AWS config for region %s: %w", region, err)
|
||||
}
|
||||
|
||||
// Create clients with the config
|
||||
a.client = bedrockruntime.NewFromConfig(cfg)
|
||||
a.mgmtClient = bedrock.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
// Handle model selection based on input type
|
||||
if validateInferenceProfileArn(modelInput) {
|
||||
// Get the inference profile details
|
||||
profile, err := a.getInferenceProfile(context.Background(), modelInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get inference profile: %v", err)
|
||||
}
|
||||
// Extract the model ID from the inference profile
|
||||
modelID, err := a.extractModelFromInferenceProfile(profile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract model ID from inference profile: %v", err)
|
||||
}
|
||||
// Find the model configuration for the extracted model ID
|
||||
foundModel, err := a.getModelFromString(modelID)
|
||||
if err != nil {
|
||||
// Instead of failing, use a generic config for completion/response
|
||||
// But still warn user
|
||||
return fmt.Errorf("failed to find model configuration for %s: %v", modelID, err)
|
||||
}
|
||||
// Use the found model config for completion/response, but set ModelName to the profile ARN
|
||||
a.model = foundModel
|
||||
a.model.Config.ModelName = modelInput
|
||||
// Mark that we're using an inference profile
|
||||
// (could add a field if needed)
|
||||
} else {
|
||||
// Regular model ID provided
|
||||
foundModel, err := a.getModelFromString(modelInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("model '%s' is not supported: %v", modelInput, err)
|
||||
}
|
||||
a.model = foundModel
|
||||
a.model.Config.ModelName = foundModel.Config.ModelName
|
||||
}
|
||||
|
||||
// Set common configuration parameters
|
||||
a.temperature = config.GetTemperature()
|
||||
a.topP = config.GetTopP()
|
||||
a.maxTokens = config.GetMaxTokens()
|
||||
@@ -119,41 +495,84 @@ func (a *AmazonBedRockClient) Configure(config IAIConfig) error {
|
||||
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 based on the model & the Response Body per model as well.
|
||||
var request map[string]interface{}
|
||||
switch a.model {
|
||||
case ModelAnthropicClaudeV2, ModelAnthropicClaudeV1, ModelAnthropicClaudeInstantV1:
|
||||
request = map[string]interface{}{
|
||||
"prompt": fmt.Sprintf("\n\nHuman: %s \n\nAssistant:", prompt),
|
||||
"max_tokens_to_sample": a.maxTokens,
|
||||
"temperature": a.temperature,
|
||||
"top_p": a.topP,
|
||||
}
|
||||
case ModelA21J2UltraV1, ModelA21J2JumboInstruct:
|
||||
request = map[string]interface{}{
|
||||
"prompt": prompt,
|
||||
"maxTokens": a.maxTokens,
|
||||
"temperature": a.temperature,
|
||||
"topP": a.topP,
|
||||
}
|
||||
case ModelAmazonTitanExpressV1:
|
||||
request = map[string]interface{}{
|
||||
"inputText": fmt.Sprintf("\n\nUser: %s", prompt),
|
||||
"textGenerationConfig": map[string]interface{}{
|
||||
"maxTokenCount": a.maxTokens,
|
||||
"temperature": a.temperature,
|
||||
"topP": a.topP,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("model %s not supported", a.model)
|
||||
// getInferenceProfile retrieves the inference profile details from Amazon Bedrock
|
||||
func (a *AmazonBedRockClient) getInferenceProfile(ctx context.Context, inferenceProfileARN string) (*bedrock.GetInferenceProfileOutput, error) {
|
||||
// Extract the profile ID from the ARN
|
||||
// ARN format: arn:aws:bedrock:region:account-id:inference-profile/profile-id
|
||||
// or arn:aws:bedrock:region:account-id:application-inference-profile/profile-id
|
||||
parts := strings.Split(inferenceProfileARN, "/")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid inference profile ARN format: %s", inferenceProfileARN)
|
||||
}
|
||||
|
||||
profileID := parts[1]
|
||||
|
||||
body, err := json.Marshal(request)
|
||||
// Create the input for the GetInferenceProfile API call
|
||||
input := &bedrock.GetInferenceProfileInput{
|
||||
InferenceProfileIdentifier: aws.String(profileID),
|
||||
}
|
||||
|
||||
// Call the GetInferenceProfile API
|
||||
output, err := a.mgmtClient.GetInferenceProfile(ctx, input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get inference profile: %w", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// extractModelFromInferenceProfile extracts the model ID from the inference profile
|
||||
func (a *AmazonBedRockClient) extractModelFromInferenceProfile(profile *bedrock.GetInferenceProfileOutput) (string, error) {
|
||||
if profile == nil || len(profile.Models) == 0 {
|
||||
return "", fmt.Errorf("inference profile does not contain any models")
|
||||
}
|
||||
|
||||
// Check if the first model has a non-nil ModelArn
|
||||
if profile.Models[0].ModelArn == nil {
|
||||
return "", fmt.Errorf("model information is missing in inference profile")
|
||||
}
|
||||
|
||||
// Get the first model ARN from the profile
|
||||
modelARN := aws.ToString(profile.Models[0].ModelArn)
|
||||
if modelARN == "" {
|
||||
return "", fmt.Errorf("model ARN is empty in inference profile")
|
||||
}
|
||||
|
||||
// Extract the model ID from the ARN
|
||||
// ARN format: arn:aws:bedrock:region::foundation-model/model-id
|
||||
parts := strings.Split(modelARN, "/")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid model ARN format: %s", modelARN)
|
||||
}
|
||||
|
||||
modelID := parts[1]
|
||||
return modelID, 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) {
|
||||
// override config defaults
|
||||
a.model.Config.MaxTokens = a.maxTokens
|
||||
a.model.Config.Temperature = a.temperature
|
||||
a.model.Config.TopP = a.topP
|
||||
|
||||
supportedModels := make([]string, len(a.models))
|
||||
for i, m := range a.models {
|
||||
supportedModels[i] = m.Name
|
||||
}
|
||||
|
||||
// Allow valid inference profile ARNs as supported models
|
||||
if !bedrock_support.IsModelSupported(a.model.Config.ModelName, supportedModels) && !validateInferenceProfileArn(a.model.Config.ModelName) {
|
||||
return "", fmt.Errorf("model '%s' is not supported.\nSupported models:\n%s", a.model.Config.ModelName, func() string {
|
||||
s := ""
|
||||
for _, m := range supportedModels {
|
||||
s += " - " + m + "\n"
|
||||
}
|
||||
return s
|
||||
}())
|
||||
}
|
||||
|
||||
body, err := a.model.Completion.GetCompletion(ctx, prompt, a.model.Config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -161,66 +580,46 @@ func (a *AmazonBedRockClient) GetCompletion(ctx context.Context, prompt string)
|
||||
// Build the parameters for the model invocation
|
||||
params := &bedrockruntime.InvokeModelInput{
|
||||
Body: body,
|
||||
ModelId: aws.String(a.model),
|
||||
ModelId: aws.String(a.model.Config.ModelName),
|
||||
ContentType: aws.String("application/json"),
|
||||
Accept: aws.String("application/json"),
|
||||
}
|
||||
// Invoke the model
|
||||
resp, err := a.client.InvokeModelWithContext(ctx, params)
|
||||
|
||||
// Detect if the model name is an inference profile ARN and set the header if so
|
||||
var optFns []func(*bedrockruntime.Options)
|
||||
if validateInferenceProfileArn(a.model.Config.ModelName) {
|
||||
inferenceProfileArn := a.model.Config.ModelName
|
||||
optFns = append(optFns, func(options *bedrockruntime.Options) {
|
||||
options.APIOptions = append(options.APIOptions, func(stack *middleware.Stack) error {
|
||||
return stack.Initialize.Add(middleware.InitializeMiddlewareFunc("InferenceProfileHeader", func(ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) (out middleware.InitializeOutput, metadata middleware.Metadata, err error) {
|
||||
req, ok := in.Parameters.(*smithyhttp.Request)
|
||||
if ok {
|
||||
req.Header.Set("X-Amzn-Bedrock-Inference-Profile-ARN", inferenceProfileArn)
|
||||
}
|
||||
return next.HandleInitialize(ctx, in)
|
||||
}), middleware.Before)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Invoke the model
|
||||
var resp *bedrockruntime.InvokeModelOutput
|
||||
if len(optFns) > 0 {
|
||||
resp, err = a.client.InvokeModel(ctx, params, optFns...)
|
||||
} else {
|
||||
resp, err = a.client.InvokeModel(ctx, params)
|
||||
}
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "SignatureDoesNotMatch") || strings.Contains(err.Error(), "NoCredentialProviders") {
|
||||
return "", fmt.Errorf("AWS credentials are invalid or missing. Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or AWS config. Details: %v", err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Response type changes as per model
|
||||
switch a.model {
|
||||
case ModelAnthropicClaudeV2, ModelAnthropicClaudeV1, ModelAnthropicClaudeInstantV1:
|
||||
type InvokeModelResponseBody struct {
|
||||
Completion string `json:"completion"`
|
||||
Stop_reason string `json:"stop_reason"`
|
||||
}
|
||||
output := &InvokeModelResponseBody{}
|
||||
err = json.Unmarshal(resp.Body, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output.Completion, nil
|
||||
case ModelA21J2UltraV1, ModelA21J2JumboInstruct:
|
||||
type Data struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
type Completion struct {
|
||||
Data Data `json:"data"`
|
||||
}
|
||||
type InvokeModelResponseBody struct {
|
||||
Completions []Completion `json:"completions"`
|
||||
}
|
||||
output := &InvokeModelResponseBody{}
|
||||
err = json.Unmarshal(resp.Body, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output.Completions[0].Data.Text, nil
|
||||
case ModelAmazonTitanExpressV1:
|
||||
type Result struct {
|
||||
TokenCount int `json:"tokenCount"`
|
||||
OutputText string `json:"outputText"`
|
||||
CompletionReason string `json:"completionReason"`
|
||||
}
|
||||
type InvokeModelResponseBody struct {
|
||||
InputTextTokenCount int `json:"inputTextTokenCount"`
|
||||
Results []Result `json:"results"`
|
||||
}
|
||||
output := &InvokeModelResponseBody{}
|
||||
err = json.Unmarshal(resp.Body, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output.Results[0].OutputText, nil
|
||||
default:
|
||||
return "", fmt.Errorf("model %s not supported", a.model)
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
return a.model.Response.ParseResponse(resp.Body)
|
||||
}
|
||||
|
||||
// GetName returns the name of the AmazonBedRockClient.
|
||||
func (a *AmazonBedRockClient) GetName() string {
|
||||
return amazonbedrockAIClientName
|
||||
|
||||
103
pkg/ai/amazonbedrock_mock_test.go
Normal file
103
pkg/ai/amazonbedrock_mock_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/bedrock"
|
||||
"github.com/aws/aws-sdk-go-v2/service/bedrock/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Mock for Bedrock Management Client
|
||||
type MockBedrockClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockBedrockClient) GetInferenceProfile(ctx context.Context, params *bedrock.GetInferenceProfileInput, optFns ...func(*bedrock.Options)) (*bedrock.GetInferenceProfileOutput, error) {
|
||||
args := m.Called(ctx, params)
|
||||
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return args.Get(0).(*bedrock.GetInferenceProfileOutput), args.Error(1)
|
||||
}
|
||||
|
||||
// Mock for Bedrock Runtime Client
|
||||
type MockBedrockRuntimeClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockBedrockRuntimeClient) InvokeModel(ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error) {
|
||||
args := m.Called(ctx, params)
|
||||
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
|
||||
return args.Get(0).(*bedrockruntime.InvokeModelOutput), args.Error(1)
|
||||
}
|
||||
|
||||
// TestBedrockInferenceProfileARNWithMocks tests the inference profile ARN validation with mocks
|
||||
func TestBedrockInferenceProfileARNWithMocks(t *testing.T) {
|
||||
// Create test models
|
||||
testModels := []bedrock_support.BedrockModel{
|
||||
{
|
||||
Name: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create a client with test models
|
||||
client := &AmazonBedRockClient{models: testModels}
|
||||
|
||||
// Create mock clients
|
||||
mockMgmtClient := new(MockBedrockClient)
|
||||
mockRuntimeClient := new(MockBedrockRuntimeClient)
|
||||
|
||||
// Inject mock clients into the AmazonBedRockClient
|
||||
client.mgmtClient = mockMgmtClient
|
||||
client.client = mockRuntimeClient
|
||||
|
||||
// Test with a valid inference profile ARN
|
||||
inferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile"
|
||||
|
||||
// Setup mock response for GetInferenceProfile
|
||||
mockMgmtClient.On("GetInferenceProfile", mock.Anything, &bedrock.GetInferenceProfileInput{
|
||||
InferenceProfileIdentifier: aws.String("my-profile"),
|
||||
}).Return(&bedrock.GetInferenceProfileOutput{
|
||||
Models: []types.InferenceProfileModel{
|
||||
{
|
||||
ModelArn: aws.String("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0"),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
// Configure the client with the inference profile ARN
|
||||
config := AIProvider{
|
||||
Model: inferenceProfileARN,
|
||||
ProviderRegion: "us-east-1",
|
||||
}
|
||||
|
||||
// Test the Configure method with the inference profile ARN
|
||||
err := client.Configure(&config)
|
||||
|
||||
// Verify that the configuration was successful
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, inferenceProfileARN, client.model.Config.ModelName)
|
||||
|
||||
// Verify that the mock was called
|
||||
mockMgmtClient.AssertExpectations(t)
|
||||
}
|
||||
262
pkg/ai/amazonbedrock_test.go
Normal file
262
pkg/ai/amazonbedrock_test.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test models for unit testing
|
||||
var testModels = []bedrock_support.BedrockModel{
|
||||
{
|
||||
Name: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
Completion: &bedrock_support.CohereCompletion{},
|
||||
Response: &bedrock_support.CohereResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
Completion: &bedrock_support.CohereCompletion{},
|
||||
Response: &bedrock_support.CohereResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestBedrockModelConfig(t *testing.T) {
|
||||
client := &AmazonBedRockClient{models: testModels}
|
||||
|
||||
// Should return error for ARN input (no exact match)
|
||||
_, err := client.getModelFromString("arn:aws:bedrock:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0")
|
||||
assert.NotNil(t, err, "Should return error for ARN input")
|
||||
}
|
||||
|
||||
func TestBedrockInvalidModel(t *testing.T) {
|
||||
client := &AmazonBedRockClient{models: testModels}
|
||||
|
||||
// Should return error for invalid model name
|
||||
_, err := client.getModelFromString("arn:aws:s3:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0")
|
||||
assert.NotNil(t, err, "Should return error for invalid model name")
|
||||
}
|
||||
|
||||
func TestBedrockInferenceProfileARN(t *testing.T) {
|
||||
// Create a mock client with test models
|
||||
client := &AmazonBedRockClient{models: testModels}
|
||||
|
||||
// Test with a valid inference profile ARN
|
||||
inferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile"
|
||||
config := AIProvider{
|
||||
Model: inferenceProfileARN,
|
||||
ProviderRegion: "us-east-1",
|
||||
}
|
||||
|
||||
// This will fail in a real environment without mocks, but we're just testing the validation logic
|
||||
err := client.Configure(&config)
|
||||
// We expect an error since we can't actually call AWS in tests
|
||||
assert.NotNil(t, err, "Error should not be nil without AWS mocks")
|
||||
|
||||
// Test with a valid application inference profile ARN
|
||||
appInferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile"
|
||||
config = AIProvider{
|
||||
Model: appInferenceProfileARN,
|
||||
ProviderRegion: "us-east-1",
|
||||
}
|
||||
|
||||
// This will fail in a real environment without mocks, but we're just testing the validation logic
|
||||
err = client.Configure(&config)
|
||||
// We expect an error since we can't actually call AWS in tests
|
||||
assert.NotNil(t, err, "Error should not be nil without AWS mocks")
|
||||
|
||||
// Test with an invalid inference profile ARN format
|
||||
invalidARN := "arn:aws:bedrock:us-east-1:123456789012:invalid-resource/my-profile"
|
||||
config = AIProvider{
|
||||
Model: invalidARN,
|
||||
ProviderRegion: "us-east-1",
|
||||
}
|
||||
|
||||
err = client.Configure(&config)
|
||||
assert.NotNil(t, err, "Error should not be nil for invalid inference profile ARN format")
|
||||
}
|
||||
|
||||
func TestBedrockGetCompletionInferenceProfile(t *testing.T) {
|
||||
modelName := "arn:aws:bedrock:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0"
|
||||
var inferenceModelModels = []bedrock_support.BedrockModel{
|
||||
{
|
||||
Name: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
Completion: &bedrock_support.CohereMessagesCompletion{},
|
||||
Response: &bedrock_support.CohereMessagesResponse{},
|
||||
Config: bedrock_support.BedrockModelConfig{
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.9,
|
||||
ModelName: modelName,
|
||||
},
|
||||
},
|
||||
}
|
||||
client := &AmazonBedRockClient{models: inferenceModelModels}
|
||||
|
||||
config := AIProvider{
|
||||
Model: modelName,
|
||||
}
|
||||
err := client.Configure(&config)
|
||||
assert.Nil(t, err, "Error should be nil")
|
||||
assert.Equal(t, modelName, client.model.Config.ModelName, "Model name should match")
|
||||
}
|
||||
|
||||
func TestGetModelFromString(t *testing.T) {
|
||||
client := &AmazonBedRockClient{models: testModels}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
model string
|
||||
wantModel string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "exact model name match",
|
||||
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "partial model name match",
|
||||
model: "claude-3-5-sonnet",
|
||||
wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "model name with different version",
|
||||
model: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
wantModel: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non-existent model",
|
||||
model: "non-existent-model",
|
||||
wantModel: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty model name",
|
||||
model: "",
|
||||
wantModel: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "model name with extra spaces",
|
||||
model: " anthropic.claude-3-5-sonnet-20240620-v1:0 ",
|
||||
wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "case insensitive match",
|
||||
model: "ANTHROPIC.CLAUDE-3-5-SONNET-20240620-V1:0",
|
||||
wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotModel, err := client.getModelFromString(tt.model)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getModelFromString() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && gotModel.Name != tt.wantModel {
|
||||
t.Errorf("getModelFromString() = %v, want %v", gotModel.Name, tt.wantModel)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultModels tests that the client works with default models
|
||||
func TestDefaultModels(t *testing.T) {
|
||||
client := &AmazonBedRockClient{}
|
||||
|
||||
// Configure should initialize default models
|
||||
err := client.Configure(&AIProvider{
|
||||
Model: "anthropic.claude-v2",
|
||||
})
|
||||
|
||||
assert.NoError(t, err, "Configure should not return an error")
|
||||
assert.NotNil(t, client.models, "Models should be initialized")
|
||||
assert.NotEmpty(t, client.models, "Models should not be empty")
|
||||
|
||||
// Test finding a default model
|
||||
model, err := client.getModelFromString("anthropic.claude-v2")
|
||||
assert.NoError(t, err, "Should find the model")
|
||||
assert.Equal(t, "anthropic.claude-v2", model.Name, "Should find the correct model")
|
||||
}
|
||||
|
||||
func TestValidateInferenceProfileArn(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arn string
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "valid inference profile ARN",
|
||||
arn: "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "valid application inference profile ARN",
|
||||
arn: "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile",
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "invalid service in ARN",
|
||||
arn: "arn:aws:s3:us-east-1:123456789012:inference-profile/my-profile",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "invalid resource type in ARN",
|
||||
arn: "arn:aws:bedrock:us-east-1:123456789012:model/my-profile",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "malformed ARN",
|
||||
arn: "arn:aws:bedrock:us-east-1:inference-profile/my-profile",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "not an ARN",
|
||||
arn: "not-an-arn",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
arn: "",
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := validateInferenceProfileArn(tt.arn)
|
||||
assert.Equal(t, tt.valid, result, "validateInferenceProfileArn() result should match expected")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ const azureAIClientName = "azureopenai"
|
||||
type AzureAIClient struct {
|
||||
nopCloser
|
||||
|
||||
client *openai.Client
|
||||
model string
|
||||
temperature float32
|
||||
client *openai.Client
|
||||
model string
|
||||
temperature float32
|
||||
// organizationId string
|
||||
}
|
||||
|
||||
|
||||
18
pkg/ai/bedrock_interfaces.go
Normal file
18
pkg/ai/bedrock_interfaces.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/bedrock"
|
||||
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
|
||||
)
|
||||
|
||||
// BedrockManagementAPI defines the interface for Bedrock management operations
|
||||
type BedrockManagementAPI interface {
|
||||
GetInferenceProfile(ctx context.Context, params *bedrock.GetInferenceProfileInput, optFns ...func(*bedrock.Options)) (*bedrock.GetInferenceProfileOutput, error)
|
||||
}
|
||||
|
||||
// BedrockRuntimeAPI defines the interface for Bedrock runtime operations
|
||||
type BedrockRuntimeAPI interface {
|
||||
InvokeModel(ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error)
|
||||
}
|
||||
140
pkg/ai/bedrock_support/completions.go
Normal file
140
pkg/ai/bedrock_support/completions.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package bedrock_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ICompletion interface {
|
||||
GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error)
|
||||
}
|
||||
|
||||
type CohereCompletion struct {
|
||||
completion ICompletion
|
||||
}
|
||||
|
||||
func (a *CohereCompletion) GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) {
|
||||
request := map[string]interface{}{
|
||||
"prompt": fmt.Sprintf("\n\nHuman: %s \n\nAssistant:", prompt),
|
||||
"max_tokens_to_sample": modelConfig.MaxTokens,
|
||||
"temperature": modelConfig.Temperature,
|
||||
"top_p": modelConfig.TopP,
|
||||
}
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
type CohereMessagesCompletion struct {
|
||||
completion ICompletion
|
||||
}
|
||||
|
||||
func (a *CohereMessagesCompletion) GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) {
|
||||
request := map[string]interface{}{
|
||||
"max_tokens": modelConfig.MaxTokens,
|
||||
"temperature": modelConfig.Temperature,
|
||||
"top_p": modelConfig.TopP,
|
||||
"anthropic_version": "bedrock-2023-05-31", // Or another valid version
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
type AI21 struct {
|
||||
completion ICompletion
|
||||
}
|
||||
|
||||
func (a *AI21) GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) {
|
||||
request := map[string]interface{}{
|
||||
"prompt": prompt,
|
||||
"maxTokens": modelConfig.MaxTokens,
|
||||
"temperature": modelConfig.Temperature,
|
||||
"topP": modelConfig.TopP,
|
||||
}
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
type AmazonCompletion struct {
|
||||
completion ICompletion
|
||||
}
|
||||
|
||||
// Accepts a list of supported model names
|
||||
func IsModelSupported(modelName string, supportedModels []string) bool {
|
||||
for _, supportedModel := range supportedModels {
|
||||
if strings.EqualFold(modelName, supportedModel) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Note: The caller should check model support before calling GetCompletion.
|
||||
func (a *AmazonCompletion) GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) {
|
||||
if a == nil || modelConfig.ModelName == "" {
|
||||
return nil, fmt.Errorf("no model name provided to Bedrock completion")
|
||||
}
|
||||
if strings.Contains(modelConfig.ModelName, "nova") {
|
||||
return a.GetNovaCompletion(ctx, prompt, modelConfig)
|
||||
} else {
|
||||
return a.GetDefaultCompletion(ctx, prompt, modelConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AmazonCompletion) GetDefaultCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) {
|
||||
request := map[string]interface{}{
|
||||
"inputText": fmt.Sprintf("\n\nUser: %s", prompt),
|
||||
"textGenerationConfig": map[string]interface{}{
|
||||
"maxTokenCount": modelConfig.MaxTokens,
|
||||
"temperature": modelConfig.Temperature,
|
||||
"topP": modelConfig.TopP,
|
||||
},
|
||||
}
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (a *AmazonCompletion) GetNovaCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) {
|
||||
request := map[string]interface{}{
|
||||
"inferenceConfig": map[string]interface{}{
|
||||
"max_new_tokens": modelConfig.MaxTokens,
|
||||
"temperature": modelConfig.Temperature,
|
||||
"topP": modelConfig.TopP,
|
||||
},
|
||||
"messages": []map[string]interface{}{
|
||||
{
|
||||
"role": "user",
|
||||
"content": []map[string]interface{}{
|
||||
{
|
||||
"text": prompt,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
182
pkg/ai/bedrock_support/completions_test.go
Normal file
182
pkg/ai/bedrock_support/completions_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package bedrock_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCohereCompletion_GetCompletion(t *testing.T) {
|
||||
completion := &CohereCompletion{}
|
||||
modelConfig := BedrockModelConfig{
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.7,
|
||||
TopP: 0.9,
|
||||
}
|
||||
prompt := "Test prompt"
|
||||
|
||||
body, err := completion.GetCompletion(context.Background(), prompt, modelConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var request map[string]interface{}
|
||||
err = json.Unmarshal(body, &request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "\n\nHuman: Test prompt \n\nAssistant:", request["prompt"])
|
||||
assert.Equal(t, 100, int(request["max_tokens_to_sample"].(float64)))
|
||||
assert.Equal(t, 0.7, request["temperature"])
|
||||
assert.Equal(t, 0.9, request["top_p"])
|
||||
}
|
||||
|
||||
func TestAI21_GetCompletion(t *testing.T) {
|
||||
completion := &AI21{}
|
||||
modelConfig := BedrockModelConfig{
|
||||
MaxTokens: 150,
|
||||
Temperature: 0.6,
|
||||
TopP: 0.8,
|
||||
}
|
||||
prompt := "Another test prompt"
|
||||
|
||||
body, err := completion.GetCompletion(context.Background(), prompt, modelConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var request map[string]interface{}
|
||||
err = json.Unmarshal(body, &request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Another test prompt", request["prompt"])
|
||||
assert.Equal(t, 150, int(request["maxTokens"].(float64)))
|
||||
assert.Equal(t, 0.6, request["temperature"])
|
||||
assert.Equal(t, 0.8, request["topP"])
|
||||
}
|
||||
|
||||
func TestAmazonCompletion_GetDefaultCompletion(t *testing.T) {
|
||||
completion := &AmazonCompletion{}
|
||||
modelConfig := BedrockModelConfig{
|
||||
MaxTokens: 200,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.7,
|
||||
ModelName: "amazon.titan-text-express-v1",
|
||||
}
|
||||
prompt := "Default test prompt"
|
||||
|
||||
body, err := completion.GetDefaultCompletion(context.Background(), prompt, modelConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var request map[string]interface{}
|
||||
err = json.Unmarshal(body, &request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "\n\nUser: Default test prompt", request["inputText"])
|
||||
textConfig := request["textGenerationConfig"].(map[string]interface{})
|
||||
assert.Equal(t, 200, int(textConfig["maxTokenCount"].(float64)))
|
||||
assert.Equal(t, 0.5, textConfig["temperature"])
|
||||
assert.Equal(t, 0.7, textConfig["topP"])
|
||||
}
|
||||
|
||||
func TestAmazonCompletion_GetNovaCompletion(t *testing.T) {
|
||||
completion := &AmazonCompletion{}
|
||||
modelConfig := BedrockModelConfig{
|
||||
MaxTokens: 250,
|
||||
Temperature: 0.4,
|
||||
TopP: 0.6,
|
||||
ModelName: "amazon.nova-pro-v1:0",
|
||||
}
|
||||
prompt := "Nova test prompt"
|
||||
|
||||
body, err := completion.GetNovaCompletion(context.Background(), prompt, modelConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var request map[string]interface{}
|
||||
err = json.Unmarshal(body, &request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
inferenceConfig := request["inferenceConfig"].(map[string]interface{})
|
||||
assert.Equal(t, 250, int(inferenceConfig["max_new_tokens"].(float64)))
|
||||
assert.Equal(t, 0.4, inferenceConfig["temperature"])
|
||||
assert.Equal(t, 0.6, inferenceConfig["topP"])
|
||||
|
||||
messages := request["messages"].([]interface{})
|
||||
message := messages[0].(map[string]interface{})
|
||||
content := message["content"].([]interface{})
|
||||
contentMap := content[0].(map[string]interface{})
|
||||
assert.Equal(t, "Nova test prompt", contentMap["text"])
|
||||
}
|
||||
|
||||
func TestAmazonCompletion_GetCompletion_Nova(t *testing.T) {
|
||||
completion := &AmazonCompletion{}
|
||||
modelConfig := BedrockModelConfig{
|
||||
MaxTokens: 250,
|
||||
Temperature: 0.4,
|
||||
TopP: 0.6,
|
||||
ModelName: "amazon.nova-pro-v1:0",
|
||||
}
|
||||
prompt := "Nova test prompt"
|
||||
|
||||
body, err := completion.GetCompletion(context.Background(), prompt, modelConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var request map[string]interface{}
|
||||
err = json.Unmarshal(body, &request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
inferenceConfig := request["inferenceConfig"].(map[string]interface{})
|
||||
assert.Equal(t, 250, int(inferenceConfig["max_new_tokens"].(float64)))
|
||||
assert.Equal(t, 0.4, inferenceConfig["temperature"])
|
||||
assert.Equal(t, 0.6, inferenceConfig["topP"])
|
||||
|
||||
messages := request["messages"].([]interface{})
|
||||
message := messages[0].(map[string]interface{})
|
||||
content := message["content"].([]interface{})
|
||||
contentMap := content[0].(map[string]interface{})
|
||||
assert.Equal(t, "Nova test prompt", contentMap["text"])
|
||||
}
|
||||
|
||||
func TestAmazonCompletion_GetCompletion_Default(t *testing.T) {
|
||||
completion := &AmazonCompletion{}
|
||||
modelConfig := BedrockModelConfig{
|
||||
MaxTokens: 200,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.7,
|
||||
ModelName: "amazon.titan-text-express-v1",
|
||||
}
|
||||
prompt := "Default test prompt"
|
||||
|
||||
body, err := completion.GetCompletion(context.Background(), prompt, modelConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var request map[string]interface{}
|
||||
err = json.Unmarshal(body, &request)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "\n\nUser: Default test prompt", request["inputText"])
|
||||
textConfig := request["textGenerationConfig"].(map[string]interface{})
|
||||
assert.Equal(t, 200, int(textConfig["maxTokenCount"].(float64)))
|
||||
assert.Equal(t, 0.5, textConfig["temperature"])
|
||||
assert.Equal(t, 0.7, textConfig["topP"])
|
||||
}
|
||||
|
||||
func TestAmazonCompletion_GetCompletion_Inference_Profile(t *testing.T) {
|
||||
completion := &AmazonCompletion{}
|
||||
modelConfig := BedrockModelConfig{
|
||||
MaxTokens: 200,
|
||||
Temperature: 0.5,
|
||||
TopP: 0.7,
|
||||
ModelName: "arn:aws:bedrock:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
}
|
||||
prompt := "Test prompt"
|
||||
|
||||
_, err := completion.GetCompletion(context.Background(), prompt, modelConfig)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIsModelSupported(t *testing.T) {
|
||||
supported := []string{
|
||||
"anthropic.claude-v2",
|
||||
"anthropic.claude-v1",
|
||||
}
|
||||
assert.True(t, IsModelSupported("anthropic.claude-v2", supported))
|
||||
assert.False(t, IsModelSupported("unsupported-model", supported))
|
||||
}
|
||||
14
pkg/ai/bedrock_support/model.go
Normal file
14
pkg/ai/bedrock_support/model.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package bedrock_support
|
||||
|
||||
type BedrockModelConfig struct {
|
||||
MaxTokens int
|
||||
Temperature float32
|
||||
TopP float32
|
||||
ModelName string
|
||||
}
|
||||
type BedrockModel struct {
|
||||
Name string
|
||||
Completion ICompletion
|
||||
Response IResponse
|
||||
Config BedrockModelConfig
|
||||
}
|
||||
59
pkg/ai/bedrock_support/model_test.go
Normal file
59
pkg/ai/bedrock_support/model_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package bedrock_support
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBedrockModelConfig(t *testing.T) {
|
||||
config := BedrockModelConfig{
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.7,
|
||||
TopP: 0.9,
|
||||
ModelName: "test-model",
|
||||
}
|
||||
|
||||
assert.Equal(t, 100, config.MaxTokens)
|
||||
assert.Equal(t, float32(0.7), config.Temperature)
|
||||
assert.Equal(t, float32(0.9), config.TopP)
|
||||
assert.Equal(t, "test-model", config.ModelName)
|
||||
}
|
||||
|
||||
func TestBedrockModel(t *testing.T) {
|
||||
completion := &MockCompletion{}
|
||||
response := &MockResponse{}
|
||||
config := BedrockModelConfig{
|
||||
MaxTokens: 100,
|
||||
Temperature: 0.7,
|
||||
TopP: 0.9,
|
||||
ModelName: "test-model",
|
||||
}
|
||||
|
||||
model := BedrockModel{
|
||||
Name: "Test Model",
|
||||
Completion: completion,
|
||||
Response: response,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
assert.Equal(t, "Test Model", model.Name)
|
||||
assert.Equal(t, completion, model.Completion)
|
||||
assert.Equal(t, response, model.Response)
|
||||
assert.Equal(t, config, model.Config)
|
||||
}
|
||||
|
||||
// MockCompletion is a mock implementation of the ICompletion interface
|
||||
type MockCompletion struct{}
|
||||
|
||||
func (m *MockCompletion) GetCompletion(ctx context.Context, prompt string, config BedrockModelConfig) ([]byte, error) {
|
||||
return []byte(`{"prompt": "mock prompt"}`), nil
|
||||
}
|
||||
|
||||
// MockResponse is a mock implementation of the IResponse interface
|
||||
type MockResponse struct{}
|
||||
|
||||
func (m *MockResponse) ParseResponse(body []byte) (string, error) {
|
||||
return "mock response", nil
|
||||
}
|
||||
155
pkg/ai/bedrock_support/responses.go
Normal file
155
pkg/ai/bedrock_support/responses.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package bedrock_support
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type IResponse interface {
|
||||
ParseResponse(rawResponse []byte) (string, error)
|
||||
}
|
||||
|
||||
type CohereMessagesResponse struct {
|
||||
response IResponse
|
||||
}
|
||||
|
||||
func (a *CohereMessagesResponse) ParseResponse(rawResponse []byte) (string, error) {
|
||||
type InvokeModelResponseBody struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Model string `json:"model"`
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
StopReason string `json:"stop_reason"`
|
||||
StopSequence interface{} `json:"stop_sequence"` // Could be null
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
output := &InvokeModelResponseBody{}
|
||||
err := json.Unmarshal(rawResponse, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract the text content from the Content array
|
||||
var resultText string
|
||||
for _, content := range output.Content {
|
||||
if content.Type == "text" {
|
||||
resultText += content.Text
|
||||
}
|
||||
}
|
||||
|
||||
return resultText, nil
|
||||
}
|
||||
|
||||
type CohereResponse struct {
|
||||
response IResponse
|
||||
}
|
||||
|
||||
func (a *CohereResponse) ParseResponse(rawResponse []byte) (string, error) {
|
||||
type InvokeModelResponseBody struct {
|
||||
Completion string `json:"completion"`
|
||||
Stop_reason string `json:"stop_reason"`
|
||||
}
|
||||
output := &InvokeModelResponseBody{}
|
||||
err := json.Unmarshal(rawResponse, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output.Completion, nil
|
||||
}
|
||||
|
||||
type AI21Response struct {
|
||||
response IResponse
|
||||
}
|
||||
|
||||
func (a *AI21Response) ParseResponse(rawResponse []byte) (string, error) {
|
||||
type Data struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
type Completion struct {
|
||||
Data Data `json:"data"`
|
||||
}
|
||||
type InvokeModelResponseBody struct {
|
||||
Completions []Completion `json:"completions"`
|
||||
}
|
||||
output := &InvokeModelResponseBody{}
|
||||
err := json.Unmarshal(rawResponse, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output.Completions[0].Data.Text, nil
|
||||
}
|
||||
|
||||
type AmazonResponse struct {
|
||||
response IResponse
|
||||
}
|
||||
|
||||
type NovaResponse struct {
|
||||
response NResponse
|
||||
}
|
||||
type NResponse interface {
|
||||
ParseResponse(rawResponse []byte) (string, error)
|
||||
}
|
||||
|
||||
func (a *AmazonResponse) ParseResponse(rawResponse []byte) (string, error) {
|
||||
type Result struct {
|
||||
TokenCount int `json:"tokenCount"`
|
||||
OutputText string `json:"outputText"`
|
||||
CompletionReason string `json:"completionReason"`
|
||||
}
|
||||
type InvokeModelResponseBody struct {
|
||||
InputTextTokenCount int `json:"inputTextTokenCount"`
|
||||
Results []Result `json:"results"`
|
||||
}
|
||||
output := &InvokeModelResponseBody{}
|
||||
err := json.Unmarshal(rawResponse, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return output.Results[0].OutputText, nil
|
||||
}
|
||||
|
||||
func (a *NovaResponse) ParseResponse(rawResponse []byte) (string, error) {
|
||||
type Content struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content []Content `json:"content"`
|
||||
}
|
||||
|
||||
type UsageDetails struct {
|
||||
InputTokens int `json:"inputTokens"`
|
||||
OutputTokens int `json:"outputTokens"`
|
||||
TotalTokens int `json:"totalTokens"`
|
||||
CacheReadInputTokenCount int `json:"cacheReadInputTokenCount"`
|
||||
CacheWriteInputTokenCount int `json:"cacheWriteInputTokenCount,omitempty"`
|
||||
}
|
||||
|
||||
type AmazonNovaResponse struct {
|
||||
Output struct {
|
||||
Message Message `json:"message"`
|
||||
} `json:"output"`
|
||||
StopReason string `json:"stopReason"`
|
||||
Usage UsageDetails `json:"usage"`
|
||||
}
|
||||
|
||||
response := &AmazonNovaResponse{}
|
||||
err := json.Unmarshal(rawResponse, response)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(response.Output.Message.Content) > 0 {
|
||||
return response.Output.Message.Content[0].Text, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
65
pkg/ai/bedrock_support/responses_test.go
Normal file
65
pkg/ai/bedrock_support/responses_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package bedrock_support
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCohereResponse_ParseResponse(t *testing.T) {
|
||||
response := &CohereResponse{}
|
||||
rawResponse := []byte(`{"completion": "Test completion", "stop_reason": "max_tokens"}`)
|
||||
|
||||
result, err := response.ParseResponse(rawResponse)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Test completion", result)
|
||||
|
||||
invalidResponse := []byte(`{"completion": "Test completion", "invalid_json":]`)
|
||||
_, err = response.ParseResponse(invalidResponse)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAI21Response_ParseResponse(t *testing.T) {
|
||||
response := &AI21Response{}
|
||||
rawResponse := []byte(`{"completions": [{"data": {"text": "AI21 test"}}], "id": "123"}`)
|
||||
|
||||
result, err := response.ParseResponse(rawResponse)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "AI21 test", result)
|
||||
|
||||
invalidResponse := []byte(`{"completions": [{"data": {"text": "AI21 test"}}, "invalid_json":]`)
|
||||
_, err = response.ParseResponse(invalidResponse)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAmazonResponse_ParseResponse(t *testing.T) {
|
||||
response := &AmazonResponse{}
|
||||
rawResponse := []byte(`{"inputTextTokenCount": 10, "results": [{"tokenCount": 20, "outputText": "Amazon test", "completionReason": "stop"}]}`)
|
||||
|
||||
result, err := response.ParseResponse(rawResponse)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Amazon test", result)
|
||||
|
||||
invalidResponse := []byte(`{"inputTextTokenCount": 10, "results": [{"tokenCount": 20, "outputText": "Amazon test", "invalid_json":]`)
|
||||
_, err = response.ParseResponse(invalidResponse)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNovaResponse_ParseResponse(t *testing.T) {
|
||||
response := &NovaResponse{}
|
||||
rawResponse := []byte(`{"output": {"message": {"content": [{"text": "Nova test"}]}}, "stopReason": "stop", "usage": {"inputTokens": 10, "outputTokens": 20, "totalTokens": 30, "cacheReadInputTokenCount": 5}}`)
|
||||
|
||||
result, err := response.ParseResponse(rawResponse)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Nova test", result)
|
||||
|
||||
rawResponseEmptyContent := []byte(`{"output": {"message": {"content": []}}, "stopReason": "stop", "usage": {"inputTokens": 10, "outputTokens": 20, "totalTokens": 30, "cacheReadInputTokenCount": 5}}`)
|
||||
|
||||
resultEmptyContent, errEmptyContent := response.ParseResponse(rawResponseEmptyContent)
|
||||
assert.NoError(t, errEmptyContent)
|
||||
assert.Equal(t, "", resultEmptyContent)
|
||||
|
||||
invalidResponse := []byte(`{"output": {"message": {"content": [{"text": "Nova test"}}, "invalid_json":]`)
|
||||
_, err = response.ParseResponse(invalidResponse)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
147
pkg/ai/customrest.go
Normal file
147
pkg/ai/customrest.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const CustomRestClientName = "customrest"
|
||||
|
||||
type CustomRestClient struct {
|
||||
nopCloser
|
||||
client *http.Client
|
||||
base *url.URL
|
||||
token string
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
topK int32
|
||||
}
|
||||
|
||||
type CustomRestRequest struct {
|
||||
Model string `json:"model"`
|
||||
|
||||
// Prompt is the textual prompt to send to the model.
|
||||
Prompt string `json:"prompt"`
|
||||
|
||||
// Options lists model-specific options. For example, temperature can be
|
||||
// set through this field, if the model supports it.
|
||||
Options map[string]interface{} `json:"options"`
|
||||
}
|
||||
|
||||
type CustomRestResponse struct {
|
||||
// Model is the model name that generated the response.
|
||||
Model string `json:"model"`
|
||||
|
||||
// CreatedAt is the timestamp of the response.
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// Response is the textual response itself.
|
||||
Response string `json:"response"`
|
||||
}
|
||||
|
||||
func (c *CustomRestClient) Configure(config IAIConfig) error {
|
||||
baseURL := config.GetBaseURL()
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
}
|
||||
c.token = config.GetPassword()
|
||||
baseClientURL, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.base = baseClientURL
|
||||
|
||||
proxyEndpoint := config.GetProxyEndpoint()
|
||||
c.client = http.DefaultClient
|
||||
if proxyEndpoint != "" {
|
||||
proxyUrl, err := url.Parse(proxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transport := &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyUrl),
|
||||
}
|
||||
|
||||
c.client = &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
c.model = config.GetModel()
|
||||
if c.model == "" {
|
||||
c.model = defaultModel
|
||||
}
|
||||
c.temperature = config.GetTemperature()
|
||||
c.topP = config.GetTopP()
|
||||
c.topK = config.GetTopK()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CustomRestClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
var promptDetail struct {
|
||||
Language string `json:"language,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
}
|
||||
prompt = strings.NewReplacer("\n", "\\n", "\t", "\\t").Replace(prompt)
|
||||
if err := json.Unmarshal([]byte(prompt), &promptDetail); err != nil {
|
||||
return "", err
|
||||
}
|
||||
generateRequest := &CustomRestRequest{
|
||||
Model: c.model,
|
||||
Prompt: promptDetail.Prompt,
|
||||
Options: map[string]interface{}{
|
||||
"temperature": c.temperature,
|
||||
"top_p": c.topP,
|
||||
"top_k": c.topK,
|
||||
"message": promptDetail.Message,
|
||||
"language": promptDetail.Language,
|
||||
},
|
||||
}
|
||||
requestBody, err := json.Marshal(generateRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.base.String(), bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if c.token != "" {
|
||||
request.Header.Set("Authorization", "Bearer "+c.token)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
request.Header.Set("Accept", "application/x-ndjson")
|
||||
|
||||
response, err := c.client.Do(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
responseBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read response body: %w", err)
|
||||
}
|
||||
|
||||
if response.StatusCode >= http.StatusBadRequest {
|
||||
return "", fmt.Errorf("Request Error, StatusCode: %d, ErrorMessage: %s", response.StatusCode, responseBody)
|
||||
}
|
||||
|
||||
var result CustomRestResponse
|
||||
if err := json.Unmarshal(responseBody, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.Response, nil
|
||||
}
|
||||
|
||||
func (c *CustomRestClient) GetName() string {
|
||||
return CustomRestClientName
|
||||
}
|
||||
87
pkg/ai/factory.go
Normal file
87
pkg/ai/factory.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 ai
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// AIClientFactory is an interface for creating AI clients
|
||||
type AIClientFactory interface {
|
||||
NewClient(provider string) IAI
|
||||
}
|
||||
|
||||
// DefaultAIClientFactory is the default implementation of AIClientFactory
|
||||
type DefaultAIClientFactory struct{}
|
||||
|
||||
// NewClient creates a new AI client using the default implementation
|
||||
func (f *DefaultAIClientFactory) NewClient(provider string) IAI {
|
||||
return NewClient(provider)
|
||||
}
|
||||
|
||||
// ConfigProvider is an interface for accessing configuration
|
||||
type ConfigProvider interface {
|
||||
UnmarshalKey(key string, rawVal interface{}) error
|
||||
}
|
||||
|
||||
// ViperConfigProvider is the default implementation of ConfigProvider using Viper
|
||||
type ViperConfigProvider struct{}
|
||||
|
||||
// UnmarshalKey unmarshals a key from the configuration using Viper
|
||||
func (p *ViperConfigProvider) UnmarshalKey(key string, rawVal interface{}) error {
|
||||
return viper.UnmarshalKey(key, rawVal)
|
||||
}
|
||||
|
||||
// Default instances to be used
|
||||
var (
|
||||
DefaultClientFactory = &DefaultAIClientFactory{}
|
||||
DefaultConfigProvider = &ViperConfigProvider{}
|
||||
)
|
||||
|
||||
// For testing - these variables can be overridden in tests
|
||||
var (
|
||||
testAIClientFactory AIClientFactory = nil
|
||||
testConfigProvider ConfigProvider = nil
|
||||
)
|
||||
|
||||
// GetAIClientFactory returns the test factory if set, otherwise the default
|
||||
func GetAIClientFactory() AIClientFactory {
|
||||
if testAIClientFactory != nil {
|
||||
return testAIClientFactory
|
||||
}
|
||||
return DefaultClientFactory
|
||||
}
|
||||
|
||||
// GetConfigProvider returns the test provider if set, otherwise the default
|
||||
func GetConfigProvider() ConfigProvider {
|
||||
if testConfigProvider != nil {
|
||||
return testConfigProvider
|
||||
}
|
||||
return DefaultConfigProvider
|
||||
}
|
||||
|
||||
// For testing - set the test implementations
|
||||
func SetTestAIClientFactory(factory AIClientFactory) {
|
||||
testAIClientFactory = factory
|
||||
}
|
||||
|
||||
func SetTestConfigProvider(provider ConfigProvider) {
|
||||
testConfigProvider = provider
|
||||
}
|
||||
|
||||
// Reset test implementations
|
||||
func ResetTestImplementations() {
|
||||
testAIClientFactory = nil
|
||||
testConfigProvider = nil
|
||||
}
|
||||
@@ -80,10 +80,10 @@ func (c *GoogleGenAIClient) GetCompletion(ctx context.Context, prompt string) (s
|
||||
if !r.Blocked {
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("complection blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
|
||||
return "", fmt.Errorf("completion blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
|
||||
}
|
||||
}
|
||||
return "", errors.New("no complection returned; unknown reason")
|
||||
return "", errors.New("no completion returned; unknown reason")
|
||||
}
|
||||
|
||||
// Format output.
|
||||
|
||||
@@ -61,10 +61,22 @@ var VERTEXAI_SUPPORTED_REGION = []string{
|
||||
}
|
||||
|
||||
const (
|
||||
ModelGeminiProV1 = "gemini-1.0-pro-001"
|
||||
ModelGeminiProV1 = "gemini-1.0-pro-001" // Retired Model
|
||||
ModelGeminiProV2_5 = "gemini-2.5-pro" // Latest Stable Model
|
||||
ModelGeminiFlashV2_5 = "gemini-2.5-flash" // Latest Stable Model
|
||||
ModelGeminiFlashV2 = "gemini-2.0-flash" // Latest Stable Model
|
||||
ModelGeminiFlashLiteV2 = "gemini-2.0-flash-lite" // Latest Stable Model
|
||||
ModelGeminiProV1_5 = "gemini-1.5-pro-002*" // Legacy Stable Model
|
||||
ModelGeminiFlashV1_5 = "gemini-1.5-flash-002*" // Legacy Stable Model
|
||||
)
|
||||
|
||||
var VERTEXAI_MODELS = []string{
|
||||
ModelGeminiProV2_5,
|
||||
ModelGeminiFlashV2_5,
|
||||
ModelGeminiFlashV2,
|
||||
ModelGeminiFlashLiteV2,
|
||||
ModelGeminiProV1_5,
|
||||
ModelGeminiFlashV1_5,
|
||||
ModelGeminiProV1,
|
||||
}
|
||||
|
||||
@@ -139,10 +151,10 @@ func (g *GoogleVertexAIClient) GetCompletion(ctx context.Context, prompt string)
|
||||
if !r.Blocked {
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("complection blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
|
||||
return "", fmt.Errorf("completion blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
|
||||
}
|
||||
}
|
||||
return "", errors.New("no complection returned; unknown reason")
|
||||
return "", errors.New("no completion returned; unknown reason")
|
||||
}
|
||||
|
||||
// Format output.
|
||||
|
||||
102
pkg/ai/groq.go
Normal file
102
pkg/ai/groq.go
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
const groqAIClientName = "groq"
|
||||
|
||||
// Default Groq API endpoint (OpenAI-compatible)
|
||||
const groqAPIBaseURL = "https://api.groq.com/openai/v1"
|
||||
|
||||
type GroqClient struct {
|
||||
nopCloser
|
||||
|
||||
client *openai.Client
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
}
|
||||
|
||||
func (c *GroqClient) Configure(config IAIConfig) error {
|
||||
token := config.GetPassword()
|
||||
defaultConfig := openai.DefaultConfig(token)
|
||||
proxyEndpoint := config.GetProxyEndpoint()
|
||||
|
||||
baseURL := config.GetBaseURL()
|
||||
if baseURL != "" {
|
||||
defaultConfig.BaseURL = baseURL
|
||||
} else {
|
||||
defaultConfig.BaseURL = groqAPIBaseURL
|
||||
}
|
||||
|
||||
transport := &http.Transport{}
|
||||
if proxyEndpoint != "" {
|
||||
proxyUrl, err := url.Parse(proxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transport.Proxy = http.ProxyURL(proxyUrl)
|
||||
}
|
||||
|
||||
customHeaders := config.GetCustomHeaders()
|
||||
defaultConfig.HTTPClient = &http.Client{
|
||||
Transport: &OpenAIHeaderTransport{
|
||||
Origin: transport,
|
||||
Headers: customHeaders,
|
||||
},
|
||||
}
|
||||
|
||||
client := openai.NewClientWithConfig(defaultConfig)
|
||||
if client == nil {
|
||||
return errors.New("error creating Groq client")
|
||||
}
|
||||
c.client = client
|
||||
c.model = config.GetModel()
|
||||
c.temperature = config.GetTemperature()
|
||||
c.topP = config.GetTopP()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GroqClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
|
||||
Model: c.model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
Temperature: c.temperature,
|
||||
MaxTokens: maxToken,
|
||||
PresencePenalty: presencePenalty,
|
||||
FrequencyPenalty: frequencyPenalty,
|
||||
TopP: c.topP,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
func (c *GroqClient) GetName() string {
|
||||
return groqAIClientName
|
||||
}
|
||||
@@ -32,7 +32,9 @@ var (
|
||||
&HuggingfaceClient{},
|
||||
&GoogleVertexAIClient{},
|
||||
&OCIGenAIClient{},
|
||||
&CustomRestClient{},
|
||||
&IBMWatsonxAIClient{},
|
||||
&GroqClient{},
|
||||
}
|
||||
Backends = []string{
|
||||
openAIClientName,
|
||||
@@ -47,7 +49,9 @@ var (
|
||||
huggingfaceAIClientName,
|
||||
googleVertexAIClientName,
|
||||
ociClientName,
|
||||
CustomRestClientName,
|
||||
ibmWatsonxAIClientName,
|
||||
groqAIClientName,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -181,7 +185,7 @@ func (p *AIProvider) GetCustomHeaders() []http.Header {
|
||||
return p.CustomHeaders
|
||||
}
|
||||
|
||||
var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "googlevertexai", "oci"}
|
||||
var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "googlevertexai", "oci", "customrest"}
|
||||
|
||||
func NeedPassword(backend string) bool {
|
||||
for _, b := range passwordlessProviders {
|
||||
|
||||
@@ -16,21 +16,32 @@ package ai
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/oracle/oci-go-sdk/v65/common"
|
||||
"github.com/oracle/oci-go-sdk/v65/generativeai"
|
||||
"github.com/oracle/oci-go-sdk/v65/generativeaiinference"
|
||||
"strings"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const ociClientName = "oci"
|
||||
|
||||
type ociModelVendor string
|
||||
|
||||
const (
|
||||
vendorCohere = "cohere"
|
||||
vendorMeta = "meta"
|
||||
)
|
||||
|
||||
type OCIGenAIClient struct {
|
||||
nopCloser
|
||||
|
||||
client *generativeaiinference.GenerativeAiInferenceClient
|
||||
model string
|
||||
model *generativeai.Model
|
||||
modelID string
|
||||
compartmentId string
|
||||
temperature float32
|
||||
topP float32
|
||||
topK int32
|
||||
maxTokens int
|
||||
}
|
||||
|
||||
@@ -40,9 +51,10 @@ func (c *OCIGenAIClient) GetName() string {
|
||||
|
||||
func (c *OCIGenAIClient) Configure(config IAIConfig) error {
|
||||
config.GetEndpointName()
|
||||
c.model = config.GetModel()
|
||||
c.modelID = config.GetModel()
|
||||
c.temperature = config.GetTemperature()
|
||||
c.topP = config.GetTopP()
|
||||
c.topK = config.GetTopK()
|
||||
c.maxTokens = config.GetMaxTokens()
|
||||
c.compartmentId = config.GetCompartmentId()
|
||||
provider := common.DefaultConfigProvider()
|
||||
@@ -51,47 +63,123 @@ func (c *OCIGenAIClient) Configure(config IAIConfig) error {
|
||||
return err
|
||||
}
|
||||
c.client = &client
|
||||
model, err := c.getModel(provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.model = model
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
generateTextRequest := c.newGenerateTextRequest(prompt)
|
||||
generateTextResponse, err := c.client.GenerateText(ctx, generateTextRequest)
|
||||
request := c.newChatRequest(prompt)
|
||||
response, err := c.client.Chat(ctx, request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return extractGeneratedText(generateTextResponse.InferenceResponse)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return extractGeneratedText(response.ChatResponse)
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) newGenerateTextRequest(prompt string) generativeaiinference.GenerateTextRequest {
|
||||
temperatureF64 := float64(c.temperature)
|
||||
topPF64 := float64(c.topP)
|
||||
return generativeaiinference.GenerateTextRequest{
|
||||
GenerateTextDetails: generativeaiinference.GenerateTextDetails{
|
||||
func (c *OCIGenAIClient) newChatRequest(prompt string) generativeaiinference.ChatRequest {
|
||||
return generativeaiinference.ChatRequest{
|
||||
ChatDetails: generativeaiinference.ChatDetails{
|
||||
CompartmentId: &c.compartmentId,
|
||||
ServingMode: generativeaiinference.OnDemandServingMode{
|
||||
ModelId: &c.model,
|
||||
},
|
||||
InferenceRequest: generativeaiinference.CohereLlmInferenceRequest{
|
||||
Prompt: &prompt,
|
||||
MaxTokens: &c.maxTokens,
|
||||
Temperature: &temperatureF64,
|
||||
TopP: &topPF64,
|
||||
},
|
||||
ServingMode: c.getServingMode(),
|
||||
ChatRequest: c.getChatModelRequest(prompt),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func extractGeneratedText(llmInferenceResponse generativeaiinference.LlmInferenceResponse) (string, error) {
|
||||
response, ok := llmInferenceResponse.(generativeaiinference.CohereLlmInferenceResponse)
|
||||
if !ok {
|
||||
return "", errors.New("failed to extract generated text from backed response")
|
||||
func (c *OCIGenAIClient) getChatModelRequest(prompt string) generativeaiinference.BaseChatRequest {
|
||||
temperatureF64 := float64(c.temperature)
|
||||
topPF64 := float64(c.topP)
|
||||
topK := int(c.topK)
|
||||
|
||||
switch c.getVendor() {
|
||||
case vendorMeta:
|
||||
messages := []generativeaiinference.Message{
|
||||
generativeaiinference.UserMessage{
|
||||
Content: []generativeaiinference.ChatContent{
|
||||
generativeaiinference.TextContent{
|
||||
Text: &prompt,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
// 0 is invalid for Meta vendor type, instead use -1 to disable topK sampling.
|
||||
if topK == 0 {
|
||||
topK = -1
|
||||
}
|
||||
return generativeaiinference.GenericChatRequest{
|
||||
Messages: messages,
|
||||
TopK: &topK,
|
||||
TopP: &topPF64,
|
||||
Temperature: &temperatureF64,
|
||||
MaxTokens: &c.maxTokens,
|
||||
}
|
||||
default: // Default to cohere
|
||||
return generativeaiinference.CohereChatRequest{
|
||||
Message: &prompt,
|
||||
MaxTokens: &c.maxTokens,
|
||||
Temperature: &temperatureF64,
|
||||
TopK: &topK,
|
||||
TopP: &topPF64,
|
||||
}
|
||||
|
||||
}
|
||||
sb := strings.Builder{}
|
||||
for _, text := range response.GeneratedTexts {
|
||||
if text.Text != nil {
|
||||
sb.WriteString(*text.Text)
|
||||
}
|
||||
|
||||
func extractGeneratedText(llmInferenceResponse generativeaiinference.BaseChatResponse) (string, error) {
|
||||
switch response := llmInferenceResponse.(type) {
|
||||
case generativeaiinference.GenericChatResponse:
|
||||
if len(response.Choices) > 0 && len(response.Choices[0].Message.GetContent()) > 0 {
|
||||
if content, ok := response.Choices[0].Message.GetContent()[0].(generativeaiinference.TextContent); ok {
|
||||
return *content.Text, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("no text found in oci response")
|
||||
case generativeaiinference.CohereChatResponse:
|
||||
return *response.Text, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown oci response type: %s", reflect.TypeOf(llmInferenceResponse).Name())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) getServingMode() generativeaiinference.ServingMode {
|
||||
if c.isBaseModel() {
|
||||
return generativeaiinference.OnDemandServingMode{
|
||||
ModelId: &c.modelID,
|
||||
}
|
||||
}
|
||||
return sb.String(), nil
|
||||
return generativeaiinference.DedicatedServingMode{
|
||||
EndpointId: &c.modelID,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) getModel(provider common.ConfigurationProvider) (*generativeai.Model, error) {
|
||||
client, err := generativeai.NewGenerativeAiClientWithConfigurationProvider(provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := client.GetModel(context.Background(), generativeai.GetModelRequest{
|
||||
ModelId: &c.modelID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &response.Model, nil
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) isBaseModel() bool {
|
||||
return c.model != nil && c.model.Type == generativeai.ModelTypeBase
|
||||
}
|
||||
|
||||
func (c *OCIGenAIClient) getVendor() ociModelVendor {
|
||||
if c.model == nil || c.model.Vendor == nil {
|
||||
return ""
|
||||
}
|
||||
return ociModelVendor(*c.model.Vendor)
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ const openAIClientName = "openai"
|
||||
type OpenAIClient struct {
|
||||
nopCloser
|
||||
|
||||
client *openai.Client
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
client *openai.Client
|
||||
model string
|
||||
temperature float32
|
||||
topP float32
|
||||
// organizationId string
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func (c *OpenAIClient) GetCompletion(ctx context.Context, prompt string) (string
|
||||
},
|
||||
},
|
||||
Temperature: c.temperature,
|
||||
MaxTokens: maxToken,
|
||||
MaxCompletionTokens: maxToken,
|
||||
PresencePenalty: presencePenalty,
|
||||
FrequencyPenalty: frequencyPenalty,
|
||||
TopP: c.topP,
|
||||
|
||||
@@ -6,8 +6,6 @@ const (
|
||||
Error: {Explain error here}
|
||||
Solution: {Step by step solution here}
|
||||
`
|
||||
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.
|
||||
@@ -58,12 +56,12 @@ const (
|
||||
|
||||
Solution: {kubectl command}
|
||||
`
|
||||
raw_promt = `{"language": "%s","message": "%s","prompt": "%s"}`
|
||||
)
|
||||
|
||||
var PromptMap = map[string]string{
|
||||
"raw": raw_promt,
|
||||
"default": default_prompt,
|
||||
"VulnerabilityReport": trivy_vuln_prompt, // for Trivy integration, the key should match `Result.Kind` in pkg/common/types.go
|
||||
"ConfigAuditReport": trivy_conf_prompt,
|
||||
"PrometheusConfigValidate": prom_conf_prompt,
|
||||
"PrometheusConfigRelabelReport": prom_relabel_prompt,
|
||||
"PolicyReport": kyverno_prompt,
|
||||
|
||||
@@ -16,8 +16,10 @@ package analysis
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -33,6 +35,7 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/spf13/viper"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type Analysis struct {
|
||||
@@ -89,19 +92,35 @@ func NewAnalysis(
|
||||
// Get kubernetes client from viper.
|
||||
kubecontext := viper.GetString("kubecontext")
|
||||
kubeconfig := viper.GetString("kubeconfig")
|
||||
verbose := viper.GetBool("verbose")
|
||||
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking kubernetes client initialization.")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initialising kubernetes client: %w", err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Kubernetes client initialized, server=%s.\n", client.Config.Host)
|
||||
}
|
||||
|
||||
// Load remote cache if it is configured.
|
||||
cache, err := cache.GetCacheConfiguration()
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking cache configuration.")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Cache configuration loaded, type=%s.\n", cache.GetName())
|
||||
}
|
||||
|
||||
if noCache {
|
||||
cache.DisableCache()
|
||||
if verbose {
|
||||
fmt.Println("Debug: Cache disabled.")
|
||||
}
|
||||
}
|
||||
|
||||
a := &Analysis{
|
||||
@@ -117,12 +136,31 @@ func NewAnalysis(
|
||||
WithDoc: withDoc,
|
||||
WithStats: withStats,
|
||||
}
|
||||
if verbose {
|
||||
fmt.Print("Debug: Analysis configuration loaded, ")
|
||||
fmt.Printf("filters=%v, language=%s, ", filters, language)
|
||||
if namespace == "" {
|
||||
fmt.Printf("namespace=none, ")
|
||||
} else {
|
||||
fmt.Printf("namespace=%s, ", namespace)
|
||||
}
|
||||
if labelSelector == "" {
|
||||
fmt.Printf("labelSelector=none, ")
|
||||
} else {
|
||||
fmt.Printf("labelSelector=%s, ", labelSelector)
|
||||
}
|
||||
fmt.Printf("explain=%t, maxConcurrency=%d, ", explain, maxConcurrency)
|
||||
fmt.Printf("withDoc=%t, withStats=%t.\n", withDoc, withStats)
|
||||
}
|
||||
if !explain {
|
||||
// Return early if AI use was not requested.
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var configAI ai.AIConfiguration
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking AI configuration.")
|
||||
}
|
||||
if err := viper.UnmarshalKey("ai", &configAI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -135,10 +173,16 @@ func NewAnalysis(
|
||||
// Hence, use the default provider only if the backend is not specified by the user.
|
||||
if configAI.DefaultProvider != "" && backend == "" {
|
||||
backend = configAI.DefaultProvider
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Using default AI provider %s.\n", backend)
|
||||
}
|
||||
}
|
||||
|
||||
if backend == "" {
|
||||
backend = "openai"
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Using default AI provider %s.\n", backend)
|
||||
}
|
||||
}
|
||||
|
||||
var aiProvider ai.AIProvider
|
||||
@@ -153,12 +197,23 @@ func NewAnalysis(
|
||||
return nil, fmt.Errorf("AI provider %s not specified in configuration. Please run k8sgpt auth", backend)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Debug: AI configuration loaded, provider=%s, ", backend)
|
||||
fmt.Printf("baseUrl=%s, model=%s.\n", aiProvider.BaseURL, aiProvider.Model)
|
||||
}
|
||||
|
||||
aiClient := ai.NewClient(aiProvider.Name)
|
||||
customHeaders := util.NewHeaders(httpHeaders)
|
||||
aiProvider.CustomHeaders = customHeaders
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking AI client initialization.")
|
||||
}
|
||||
if err := aiClient.Configure(&aiProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if verbose {
|
||||
fmt.Println("Debug: AI client initialized.")
|
||||
}
|
||||
a.AIClient = aiClient
|
||||
a.AnalysisAIProvider = aiProvider.Name
|
||||
return a, nil
|
||||
@@ -173,6 +228,15 @@ func (a *Analysis) CustomAnalyzersAreAvailable() bool {
|
||||
}
|
||||
|
||||
func (a *Analysis) RunCustomAnalysis() {
|
||||
// Validate namespace if specified, consistent with built-in filter behavior
|
||||
if a.Namespace != "" && a.Client != nil {
|
||||
_, err := a.Client.Client.CoreV1().Namespaces().Get(a.Context, a.Namespace, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("namespace %q not found: %s", a.Namespace, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var customAnalyzers []custom.CustomAnalyzer
|
||||
if err := viper.UnmarshalKey("custom_analyzers", &customAnalyzers); err != nil {
|
||||
a.Errors = append(a.Errors, err.Error())
|
||||
@@ -182,6 +246,18 @@ func (a *Analysis) RunCustomAnalysis() {
|
||||
semaphore := make(chan struct{}, a.MaxConcurrency)
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
verbose := viper.GetBool("verbose")
|
||||
if verbose {
|
||||
if len(customAnalyzers) == 0 {
|
||||
fmt.Println("Debug: No custom analyzers found.")
|
||||
} else {
|
||||
cAnalyzerNames := make([]string, len(customAnalyzers))
|
||||
for i, cAnalyzer := range customAnalyzers {
|
||||
cAnalyzerNames[i] = cAnalyzer.Name
|
||||
}
|
||||
fmt.Printf("Debug: Found custom analyzers %v.\n", cAnalyzerNames)
|
||||
}
|
||||
}
|
||||
for _, cAnalyzer := range customAnalyzers {
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
@@ -194,6 +270,9 @@ func (a *Analysis) RunCustomAnalysis() {
|
||||
mutex.Unlock()
|
||||
return
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s launched.\n", cAnalyzer.Name)
|
||||
}
|
||||
|
||||
result, err := canClient.Run()
|
||||
if result.Kind == "" {
|
||||
@@ -206,10 +285,16 @@ func (a *Analysis) RunCustomAnalysis() {
|
||||
mutex.Lock()
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", cAnalyzer.Name, err))
|
||||
mutex.Unlock()
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s completed with errors.\n", cAnalyzer.Name)
|
||||
}
|
||||
} else {
|
||||
mutex.Lock()
|
||||
a.Results = append(a.Results, result)
|
||||
mutex.Unlock()
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s completed without errors.\n", cAnalyzer.Name)
|
||||
}
|
||||
}
|
||||
<-semaphore
|
||||
}(cAnalyzer, &wg, semaphore)
|
||||
@@ -219,6 +304,7 @@ func (a *Analysis) RunCustomAnalysis() {
|
||||
|
||||
func (a *Analysis) RunAnalysis() {
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
verbose := viper.GetBool("verbose")
|
||||
|
||||
coreAnalyzerMap, analyzerMap := analyzer.GetAnalyzerMap()
|
||||
|
||||
@@ -227,7 +313,13 @@ func (a *Analysis) RunAnalysis() {
|
||||
if a.WithDoc {
|
||||
var openApiErr error
|
||||
|
||||
if verbose {
|
||||
fmt.Println("Debug: Fetching Kubernetes docs.")
|
||||
}
|
||||
openapiSchema, openApiErr = a.Client.Client.Discovery().OpenAPISchema()
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking Kubernetes docs.")
|
||||
}
|
||||
if openApiErr != nil {
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[KubernetesDoc] %s", openApiErr))
|
||||
}
|
||||
@@ -242,11 +334,23 @@ func (a *Analysis) RunAnalysis() {
|
||||
OpenapiSchema: openapiSchema,
|
||||
}
|
||||
|
||||
semaphore := make(chan struct{}, a.MaxConcurrency)
|
||||
// Set a reasonable maximum for concurrency to prevent excessive memory allocation
|
||||
const maxAllowedConcurrency = 100
|
||||
concurrency := a.MaxConcurrency
|
||||
if concurrency <= 0 {
|
||||
concurrency = 10 // Default value if not set
|
||||
} else if concurrency > maxAllowedConcurrency {
|
||||
concurrency = maxAllowedConcurrency // Cap at a reasonable maximum
|
||||
}
|
||||
|
||||
semaphore := make(chan struct{}, concurrency)
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
// if there are no filters selected and no active_filters then run coreAnalyzer
|
||||
if len(a.Filters) == 0 && len(activeFilters) == 0 {
|
||||
if verbose {
|
||||
fmt.Println("Debug: No filters selected and no active filters found, run all core analyzers.")
|
||||
}
|
||||
for name, analyzer := range coreAnalyzerMap {
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
@@ -258,6 +362,9 @@ func (a *Analysis) RunAnalysis() {
|
||||
}
|
||||
// if the filters flag is specified
|
||||
if len(a.Filters) != 0 {
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Filter flags %v specified, run selected core analyzers.\n", a.Filters)
|
||||
}
|
||||
for _, filter := range a.Filters {
|
||||
if analyzer, ok := analyzerMap[filter]; ok {
|
||||
semaphore <- struct{}{}
|
||||
@@ -272,6 +379,9 @@ func (a *Analysis) RunAnalysis() {
|
||||
}
|
||||
|
||||
// use active_filters
|
||||
if len(activeFilters) > 0 && verbose {
|
||||
fmt.Printf("Debug: Found active filters %v, run selected core analyzers.\n", activeFilters)
|
||||
}
|
||||
for _, filter := range activeFilters {
|
||||
if analyzer, ok := analyzerMap[filter]; ok {
|
||||
semaphore <- struct{}{}
|
||||
@@ -294,8 +404,14 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana
|
||||
}
|
||||
|
||||
// Run the analyzer
|
||||
verbose := viper.GetBool("verbose")
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s launched.\n", reflect.TypeOf(analyzer).Name())
|
||||
}
|
||||
results, err := analyzer.Analyze(analyzerConfig)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// Measure the time taken
|
||||
if a.WithStats {
|
||||
elapsedTime = time.Since(startTime)
|
||||
@@ -313,11 +429,17 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana
|
||||
a.Stats = append(a.Stats, stat)
|
||||
}
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s completed with errors.\n", reflect.TypeOf(analyzer).Name())
|
||||
}
|
||||
} else {
|
||||
if a.WithStats {
|
||||
a.Stats = append(a.Stats, stat)
|
||||
}
|
||||
a.Results = append(a.Results, results...)
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s completed without errors.\n", reflect.TypeOf(analyzer).Name())
|
||||
}
|
||||
}
|
||||
<-semaphore
|
||||
}
|
||||
@@ -327,6 +449,11 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
verbose := viper.GetBool("verbose")
|
||||
if verbose {
|
||||
fmt.Println("Debug: Generating AI analysis.")
|
||||
}
|
||||
|
||||
var bar *progressbar.ProgressBar
|
||||
if output != "json" {
|
||||
bar = progressbar.Default(int64(len(a.Results)))
|
||||
@@ -335,6 +462,10 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
for index, analysis := range a.Results {
|
||||
var texts []string
|
||||
|
||||
if bar != nil && verbose {
|
||||
bar.Describe(fmt.Sprintf("Analyzing %s", analysis.Kind))
|
||||
}
|
||||
|
||||
for _, failure := range analysis.Error {
|
||||
if anonymize {
|
||||
for _, s := range failure.Sensitive {
|
||||
@@ -405,6 +536,24 @@ func (a *Analysis) getAIResultForSanitizedFailures(texts []string, promptTmpl st
|
||||
|
||||
// Process template.
|
||||
prompt := fmt.Sprintf(strings.TrimSpace(promptTmpl), a.Language, inputKey)
|
||||
if a.AIClient.GetName() == ai.CustomRestClientName {
|
||||
// Use proper JSON marshaling to handle special characters in error messages
|
||||
// This fixes issues with quotes, newlines, and other special chars in inputKey
|
||||
customRestPrompt := struct {
|
||||
Language string `json:"language"`
|
||||
Message string `json:"message"`
|
||||
Prompt string `json:"prompt"`
|
||||
}{
|
||||
Language: a.Language,
|
||||
Message: inputKey,
|
||||
Prompt: prompt,
|
||||
}
|
||||
promptBytes, err := json.Marshal(customRestPrompt)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal customrest prompt: %w", err)
|
||||
}
|
||||
prompt = string(promptBytes)
|
||||
}
|
||||
response, err := a.AIClient.GetCompletion(a.Context, prompt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -17,13 +17,17 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/agiledragon/gomonkey/v2"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"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/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -31,9 +35,15 @@ import (
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// sub-function
|
||||
// helper function: get type name of an analyzer
|
||||
func getTypeName(i interface{}) string {
|
||||
return reflect.TypeOf(i).Name()
|
||||
}
|
||||
|
||||
// helper function: run analysis with filter
|
||||
func analysis_RunAnalysisFilterTester(t *testing.T, filterFlag string) []common.Result {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
@@ -404,3 +414,252 @@ func TestGetAIResultForSanitizedFailures(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in NewAnalysis with explain=false
|
||||
func TestVerbose_NewAnalysisWithoutExplain(t *testing.T) {
|
||||
// Set viper config.
|
||||
viper.Set("verbose", true)
|
||||
viper.Set("kubecontext", "dummy")
|
||||
viper.Set("kubeconfig", "dummy")
|
||||
|
||||
// Patch kubernetes.NewClient to return a dummy client.
|
||||
patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) {
|
||||
return &kubernetes.Client{
|
||||
Config: &rest.Config{Host: "fake-server"},
|
||||
}, nil
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
output := util.CaptureOutput(func() {
|
||||
a, err := NewAnalysis(
|
||||
"", "english", []string{"Pod"}, "default", "", true,
|
||||
false, // explain
|
||||
10, false, false, []string{}, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
a.Close()
|
||||
})
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Checking kubernetes client initialization.",
|
||||
"Debug: Kubernetes client initialized, server=fake-server.",
|
||||
"Debug: Checking cache configuration.",
|
||||
"Debug: Cache configuration loaded, type=file.",
|
||||
"Debug: Cache disabled.",
|
||||
"Debug: Analysis configuration loaded, filters=[Pod], language=english, namespace=default, labelSelector=none, explain=false, maxConcurrency=10, withDoc=false, withStats=false.",
|
||||
}
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in NewAnalysis with explain=true
|
||||
func TestVerbose_NewAnalysisWithExplain(t *testing.T) {
|
||||
// Set viper config.
|
||||
viper.Set("verbose", true)
|
||||
viper.Set("kubecontext", "dummy")
|
||||
viper.Set("kubeconfig", "dummy")
|
||||
// Set a dummy AI configuration.
|
||||
dummyAIConfig := map[string]interface{}{
|
||||
"defaultProvider": "dummy",
|
||||
"providers": []map[string]interface{}{
|
||||
{
|
||||
"name": "dummy",
|
||||
"baseUrl": "http://dummy",
|
||||
"model": "dummy-model",
|
||||
"customHeaders": map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
viper.Set("ai", dummyAIConfig)
|
||||
|
||||
// Patch kubernetes.NewClient to return a dummy client.
|
||||
patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) {
|
||||
return &kubernetes.Client{
|
||||
Config: &rest.Config{Host: "fake-server"},
|
||||
}, nil
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
// Patch ai.NewClient to return a NoOp client.
|
||||
patches2 := gomonkey.ApplyFunc(ai.NewClient, func(name string) ai.IAI {
|
||||
return &ai.NoOpAIClient{}
|
||||
})
|
||||
defer patches2.Reset()
|
||||
|
||||
output := util.CaptureOutput(func() {
|
||||
a, err := NewAnalysis(
|
||||
"", "english", []string{"Pod"}, "default", "", true,
|
||||
true, // explain
|
||||
10, false, false, []string{}, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
a.Close()
|
||||
})
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Checking AI configuration.",
|
||||
"Debug: Using default AI provider dummy.",
|
||||
"Debug: AI configuration loaded, provider=dummy, baseUrl=http://dummy, model=dummy-model.",
|
||||
"Debug: Checking AI client initialization.",
|
||||
"Debug: AI client initialized.",
|
||||
}
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunAnalysis with filter flag
|
||||
func TestVerbose_RunAnalysisWithFilter(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
// Run analysis with a filter flag ("Pod") to trigger debug output.
|
||||
output := util.CaptureOutput(func() {
|
||||
_ = analysis_RunAnalysisFilterTester(t, "Pod")
|
||||
})
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Filter flags [Pod] specified, run selected core analyzers.",
|
||||
"Debug: PodAnalyzer launched.",
|
||||
"Debug: PodAnalyzer completed without errors.",
|
||||
}
|
||||
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunAnalysis with active filter
|
||||
func TestVerbose_RunAnalysisWithActiveFilter(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
viper.SetDefault("active_filters", "Ingress")
|
||||
output := util.CaptureOutput(func() {
|
||||
_ = analysis_RunAnalysisFilterTester(t, "")
|
||||
})
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Found active filters [Ingress], run selected core analyzers.",
|
||||
"Debug: IngressAnalyzer launched.",
|
||||
"Debug: IngressAnalyzer completed without errors.",
|
||||
}
|
||||
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunAnalysis without any filter (run all core analyzers)
|
||||
func TestVerbose_RunAnalysisWithoutFilter(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
// Clear filter flag and active_filters to run all core analyzers.
|
||||
viper.SetDefault("active_filters", []string{})
|
||||
output := util.CaptureOutput(func() {
|
||||
_ = analysis_RunAnalysisFilterTester(t, "")
|
||||
})
|
||||
|
||||
// Check for debug message indicating no filters.
|
||||
expectedNoFilter := "Debug: No filters selected and no active filters found, run all core analyzers."
|
||||
if !util.Contains(output, expectedNoFilter) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedNoFilter, output)
|
||||
}
|
||||
|
||||
// Get all core analyzers from analyzer.GetAnalyzerMap()
|
||||
coreAnalyzerMap, _ := analyzer.GetAnalyzerMap()
|
||||
for _, analyzerInstance := range coreAnalyzerMap {
|
||||
analyzerType := getTypeName(analyzerInstance)
|
||||
expectedLaunched := fmt.Sprintf("Debug: %s launched.", analyzerType)
|
||||
expectedCompleted := fmt.Sprintf("Debug: %s completed without errors.", analyzerType)
|
||||
if !util.Contains(output, expectedLaunched) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedLaunched, output)
|
||||
}
|
||||
if !util.Contains(output, expectedCompleted) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedCompleted, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunCustomAnalysis without custom analyzer
|
||||
func TestVerbose_RunCustomAnalysisWithoutCustomAnalyzer(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
// Set custom_analyzers to empty array to trigger "No custom analyzers" debug message.
|
||||
viper.Set("custom_analyzers", []interface{}{})
|
||||
analysisObj := &Analysis{
|
||||
MaxConcurrency: 1,
|
||||
}
|
||||
output := util.CaptureOutput(func() {
|
||||
analysisObj.RunCustomAnalysis()
|
||||
})
|
||||
expected := "Debug: No custom analyzers found."
|
||||
if !util.Contains(output, "Debug: No custom analyzers found.") {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunCustomAnalysis with custom analyzer
|
||||
func TestVerbose_RunCustomAnalysisWithCustomAnalyzer(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
// Set custom_analyzers with one custom analyzer using "fake" connection.
|
||||
viper.Set("custom_analyzers", []map[string]interface{}{
|
||||
{
|
||||
"name": "TestCustomAnalyzer",
|
||||
"connection": map[string]interface{}{"url": "127.0.0.1", "port": "2333"},
|
||||
},
|
||||
})
|
||||
|
||||
analysisObj := &Analysis{
|
||||
MaxConcurrency: 1,
|
||||
}
|
||||
output := util.CaptureOutput(func() {
|
||||
analysisObj.RunCustomAnalysis()
|
||||
})
|
||||
assert.Equal(t, 1, len(analysisObj.Errors)) // connection error
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Found custom analyzers [TestCustomAnalyzer].",
|
||||
"Debug: TestCustomAnalyzer launched.",
|
||||
"Debug: TestCustomAnalyzer completed with errors.",
|
||||
}
|
||||
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in GetAIResults
|
||||
func TestVerbose_GetAIResults(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
disabledCache := cache.New("disabled-cache")
|
||||
disabledCache.DisableCache()
|
||||
aiClient := &ai.NoOpAIClient{}
|
||||
analysisObj := Analysis{
|
||||
AIClient: aiClient,
|
||||
Cache: disabledCache,
|
||||
Results: []common.Result{
|
||||
{
|
||||
Kind: "Deployment",
|
||||
Name: "test-deployment",
|
||||
Error: []common.Failure{{Text: "test-problem", Sensitive: []common.Sensitive{}}},
|
||||
Details: "test-solution",
|
||||
ParentObject: "parent-resource",
|
||||
},
|
||||
},
|
||||
Namespace: "default",
|
||||
}
|
||||
output := util.CaptureOutput(func() {
|
||||
_ = analysisObj.GetAIResults("json", false)
|
||||
})
|
||||
|
||||
expected := "Debug: Generating AI analysis."
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,20 +39,31 @@ var coreAnalyzerMap = map[string]common.IAnalyzer{
|
||||
"Service": ServiceAnalyzer{},
|
||||
"Ingress": IngressAnalyzer{},
|
||||
"StatefulSet": StatefulSetAnalyzer{},
|
||||
"Job": JobAnalyzer{},
|
||||
"CronJob": CronJobAnalyzer{},
|
||||
"Node": NodeAnalyzer{},
|
||||
"ValidatingWebhookConfiguration": ValidatingWebhookAnalyzer{},
|
||||
"MutatingWebhookConfiguration": MutatingWebhookAnalyzer{},
|
||||
"ConfigMap": ConfigMapAnalyzer{},
|
||||
}
|
||||
|
||||
var additionalAnalyzerMap = map[string]common.IAnalyzer{
|
||||
"HorizontalPodAutoScaler": HpaAnalyzer{},
|
||||
"HorizontalPodAutoscaler": HpaAnalyzer{},
|
||||
"PodDisruptionBudget": PdbAnalyzer{},
|
||||
"NetworkPolicy": NetworkPolicyAnalyzer{},
|
||||
"Log": LogAnalyzer{},
|
||||
"GatewayClass": GatewayClassAnalyzer{},
|
||||
"Gateway": GatewayAnalyzer{},
|
||||
"HTTPRoute": HTTPRouteAnalyzer{},
|
||||
"Storage": StorageAnalyzer{},
|
||||
"Security": SecurityAnalyzer{},
|
||||
"ClusterCatalog": ClusterCatalogAnalyzer{},
|
||||
"ClusterExtension": ClusterExtensionAnalyzer{},
|
||||
"ClusterServiceVersion": ClusterServiceVersionAnalyzer{},
|
||||
"Subscription": SubscriptionAnalyzer{},
|
||||
"InstallPlan": InstallPlanAnalyzer{},
|
||||
"CatalogSource": CatalogSourceAnalyzer{},
|
||||
"OperatorGroup": OperatorGroupAnalyzer{},
|
||||
}
|
||||
|
||||
func ListFilters() ([]string, []string, []string) {
|
||||
|
||||
53
pkg/analyzer/catalogsource.go
Normal file
53
pkg/analyzer/catalogsource.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type CatalogSourceAnalyzer struct{}
|
||||
|
||||
var catSrcGVR = schema.GroupVersionResource{
|
||||
Group: "operators.coreos.com",
|
||||
Version: "v1alpha1",
|
||||
Resource: "catalogsources",
|
||||
}
|
||||
|
||||
func (CatalogSourceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
kind := "CatalogSource"
|
||||
if a.Client.GetDynamicClient() == nil {
|
||||
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
|
||||
}
|
||||
|
||||
list, err := a.Client.GetDynamicClient().
|
||||
Resource(catSrcGVR).Namespace(metav1.NamespaceAll).
|
||||
List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []common.Result
|
||||
for _, item := range list.Items {
|
||||
ns, name := item.GetNamespace(), item.GetName()
|
||||
|
||||
state, _, _ := unstructured.NestedString(item.Object, "status", "connectionState", "lastObservedState")
|
||||
addr, _, _ := unstructured.NestedString(item.Object, "status", "connectionState", "address")
|
||||
|
||||
// Only report if state is present and not READY
|
||||
if state != "" && strings.ToUpper(state) != "READY" {
|
||||
results = append(results, common.Result{
|
||||
Kind: kind,
|
||||
Name: ns + "/" + name,
|
||||
Error: []common.Failure{{
|
||||
Text: fmt.Sprintf("connectionState=%s (address=%s)", state, addr),
|
||||
}},
|
||||
})
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
107
pkg/analyzer/catalogsource_test.go
Normal file
107
pkg/analyzer/catalogsource_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
)
|
||||
|
||||
func TestCatalogSourceAnalyzer_UnhealthyState_ReturnsResult(t *testing.T) {
|
||||
cs := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1alpha1",
|
||||
"kind": "CatalogSource",
|
||||
"metadata": map[string]any{
|
||||
"name": "broken-operators-external",
|
||||
"namespace": "openshift-marketplace",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"connectionState": map[string]any{
|
||||
"lastObservedState": "TRANSIENT_FAILURE",
|
||||
"address": "not-a-real-host.invalid:50051",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
listKinds := map[schema.GroupVersionResource]string{
|
||||
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "catalogsources"}: "CatalogSourceList",
|
||||
}
|
||||
scheme := runtime.NewScheme()
|
||||
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, cs)
|
||||
|
||||
a := common.Analyzer{
|
||||
Context: context.TODO(),
|
||||
Client: &kubernetes.Client{DynamicClient: dc},
|
||||
}
|
||||
|
||||
res, err := (CatalogSourceAnalyzer{}).Analyze(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Analyze error: %v", err)
|
||||
}
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(res))
|
||||
}
|
||||
if res[0].Kind != "CatalogSource" || !strings.Contains(res[0].Name, "openshift-marketplace/broken-operators-external") {
|
||||
t.Fatalf("unexpected result: %#v", res[0])
|
||||
}
|
||||
if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "TRANSIENT_FAILURE") {
|
||||
t.Fatalf("expected TRANSIENT_FAILURE in message, got %#v", res[0].Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogSourceAnalyzer_HealthyOrNoState_Ignored(t *testing.T) {
|
||||
// One READY (healthy), one with no status at all: both should be ignored.
|
||||
ready := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1alpha1",
|
||||
"kind": "CatalogSource",
|
||||
"metadata": map[string]any{
|
||||
"name": "ready-operators",
|
||||
"namespace": "openshift-marketplace",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"connectionState": map[string]any{
|
||||
"lastObservedState": "READY",
|
||||
"address": "somewhere",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
nostate := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1alpha1",
|
||||
"kind": "CatalogSource",
|
||||
"metadata": map[string]any{
|
||||
"name": "no-status-operators",
|
||||
"namespace": "openshift-marketplace",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
listKinds := map[schema.GroupVersionResource]string{
|
||||
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "catalogsources"}: "CatalogSourceList",
|
||||
}
|
||||
scheme := runtime.NewScheme()
|
||||
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ready, nostate)
|
||||
|
||||
a := common.Analyzer{
|
||||
Context: context.TODO(),
|
||||
Client: &kubernetes.Client{DynamicClient: dc},
|
||||
}
|
||||
|
||||
res, err := (CatalogSourceAnalyzer{}).Analyze(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Analyze error: %v", err)
|
||||
}
|
||||
if len(res) != 0 {
|
||||
t.Fatalf("expected 0 results (healthy/nostate ignored), got %d", len(res))
|
||||
}
|
||||
}
|
||||
161
pkg/analyzer/clustercatalog.go
Normal file
161
pkg/analyzer/clustercatalog.go
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type ClusterCatalogAnalyzer struct{}
|
||||
|
||||
func (ClusterCatalogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "ClusterCatalog"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
var clusterCatalogGVR = schema.GroupVersionResource{
|
||||
Group: "olm.operatorframework.io",
|
||||
Version: "v1",
|
||||
Resource: "clustercatalogs",
|
||||
}
|
||||
if a.Client == nil {
|
||||
return nil, fmt.Errorf("client is nil in ClusterCatalogAnalyzer")
|
||||
}
|
||||
if a.Client.GetDynamicClient() == nil {
|
||||
return nil, fmt.Errorf("dynamic client is nil in ClusterCatalogAnalyzer")
|
||||
}
|
||||
|
||||
list, err := a.Client.GetDynamicClient().Resource(clusterCatalogGVR).Namespace("").List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
for _, item := range list.Items {
|
||||
var failures []common.Failure
|
||||
catalog, err := ConvertToClusterCatalog(&item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("ClusterCatalog: %s | Source: %s\n", catalog.Name, catalog.Spec.Source.Image.Ref)
|
||||
failures, err = ValidateClusterCatalog(failures, catalog)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[catalog.Name] = common.PreAnalysis{
|
||||
Catalog: *catalog,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, catalog.Name, "").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.Node.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, err
|
||||
}
|
||||
|
||||
func ConvertToClusterCatalog(u *unstructured.Unstructured) (*common.ClusterCatalog, error) {
|
||||
var cc common.ClusterCatalog
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &cc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert to ClusterCatalog: %w", err)
|
||||
}
|
||||
return &cc, nil
|
||||
}
|
||||
|
||||
func addCatalogConditionFailure(failures []common.Failure, catalogName string, catalogCondition metav1.Condition) []common.Failure {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("OLMv1 ClusterCatalog: %s has condition of type %s, reason %s: %s", catalogName, catalogCondition.Type, catalogCondition.Reason, catalogCondition.Message),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: catalogName,
|
||||
Masked: util.MaskString(catalogName),
|
||||
},
|
||||
},
|
||||
})
|
||||
return failures
|
||||
}
|
||||
|
||||
func addCatalogFailure(failures []common.Failure, catalogName string, err error) []common.Failure {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("%s has error: %s", catalogName, err.Error()),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: catalogName,
|
||||
Masked: util.MaskString(catalogName),
|
||||
},
|
||||
},
|
||||
})
|
||||
return failures
|
||||
}
|
||||
|
||||
func ValidateClusterCatalog(failures []common.Failure, catalog *common.ClusterCatalog) ([]common.Failure, error) {
|
||||
if !isValidImageRef(catalog.Spec.Source.Image.Ref) {
|
||||
failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("invalid image ref format in spec.source.image.ref: %s", catalog.Spec.Source.Image.Ref))
|
||||
}
|
||||
|
||||
// Check status.resolvedSource.image.ref ends with @sha256:...
|
||||
if catalog.Status.ResolvedSource != nil {
|
||||
if catalog.Status.ResolvedSource.Image.Ref == "" {
|
||||
failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("missing status.resolvedSource.image.ref"))
|
||||
}
|
||||
if !regexp.MustCompile(`@sha256:[a-f0-9]{64}$`).MatchString(catalog.Status.ResolvedSource.Image.Ref) {
|
||||
failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("status.resolvedSource.image.ref must end with @sha256:<digest>"))
|
||||
}
|
||||
}
|
||||
|
||||
for _, condition := range catalog.Status.Conditions {
|
||||
if condition.Status != "True" && condition.Type == "Serving" {
|
||||
failures = addCatalogConditionFailure(failures, catalog.Name, condition)
|
||||
}
|
||||
if condition.Type == "Progressing" && condition.Reason != "Succeeded" {
|
||||
failures = addCatalogConditionFailure(failures, catalog.Name, condition)
|
||||
}
|
||||
}
|
||||
|
||||
return failures, nil
|
||||
}
|
||||
|
||||
// isValidImageRef does a simple regex check to validate image refs
|
||||
func isValidImageRef(ref string) bool {
|
||||
pattern := `^([a-zA-Z0-9\-\.]+(?::[0-9]+)?/)?([a-z0-9]+(?:[._\-\/][a-z0-9]+)*)(:[\w][\w.-]{0,127})?(?:@sha256:[a-f0-9]{64})?$`
|
||||
return regexp.MustCompile(pattern).MatchString(ref)
|
||||
}
|
||||
182
pkg/analyzer/clustercatalog_test.go
Normal file
182
pkg/analyzer/clustercatalog_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
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"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestClusterCatalogAnalyzer(t *testing.T) {
|
||||
gvr := schema.GroupVersionResource{
|
||||
Group: "olm.operatorframework.io",
|
||||
Version: "v1",
|
||||
Resource: "clustercatalogs",
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
|
||||
dynamicClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
|
||||
scheme,
|
||||
map[schema.GroupVersionResource]string{
|
||||
gvr: "ClusterCatalogList",
|
||||
},
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "olm.operatorframework.io/v1",
|
||||
"kind": "ClusterCatalog",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "Valid ClusterCatalog",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"availabilityMode": "Available",
|
||||
"source": map[string]interface{}{
|
||||
"type": "Image",
|
||||
"image": map[string]interface{}{
|
||||
"ref": "registry.redhat.io/redhat/community-operator-index:v4.19",
|
||||
"pollIntervalMinutes": float64(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Progressing",
|
||||
"status": "True",
|
||||
"reason": "Succeeded",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "Serving",
|
||||
"status": "True",
|
||||
"reason": "Available",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "olm.operatorframework.io/v1",
|
||||
"kind": "ClusterCatalog",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "Invalid availabilityMode",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"availabilityMode": "test",
|
||||
"source": map[string]interface{}{
|
||||
"type": "Image",
|
||||
"image": map[string]interface{}{
|
||||
"ref": "registry.redhat.io/redhat/community-operator-index:v4.19",
|
||||
"pollIntervalMinutes": float64(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Progressing",
|
||||
"status": "True",
|
||||
"reason": "Retrying",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "olm.operatorframework.io/v1",
|
||||
"kind": "ClusterCatalog",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "Invalid pollIntervalMinutes",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"availabilityMode": "Available",
|
||||
"source": map[string]interface{}{
|
||||
"type": "Image",
|
||||
"image": map[string]interface{}{
|
||||
"ref": "registry.redhat.io/redhat/community-operator-index:v4.19",
|
||||
"pollIntervalMinutes": float64(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Progressing",
|
||||
"status": "True",
|
||||
"reason": "Retrying",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "olm.operatorframework.io/v1",
|
||||
"kind": "ClusterCatalog",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "Invalid image reference",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"availabilityMode": "Available",
|
||||
"source": map[string]interface{}{
|
||||
"type": "Image",
|
||||
"image": map[string]interface{}{
|
||||
"ref": "quay.io/test/community-operator-index:v4.19",
|
||||
"pollIntervalMinutes": float64(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Progressing",
|
||||
"status": "True",
|
||||
"reason": "Retrying",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(),
|
||||
DynamicClient: dynamicClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "test",
|
||||
}
|
||||
|
||||
ccAnalyzer := ClusterCatalogAnalyzer{}
|
||||
results, err := ccAnalyzer.Analyze(config)
|
||||
for _, res := range results {
|
||||
fmt.Printf("Result: %s | Failures: %d\n", res.Name, len(res.Error))
|
||||
for _, err := range res.Error {
|
||||
fmt.Printf(" - %s\n", err)
|
||||
}
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(results))
|
||||
}
|
||||
148
pkg/analyzer/clusterextension.go
Normal file
148
pkg/analyzer/clusterextension.go
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
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"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type ClusterExtensionAnalyzer struct{}
|
||||
|
||||
func (ClusterExtensionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "ClusterExtension"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
var clusterExtensionGVR = schema.GroupVersionResource{
|
||||
Group: "olm.operatorframework.io",
|
||||
Version: "v1",
|
||||
Resource: "clusterextensions",
|
||||
}
|
||||
if a.Client == nil {
|
||||
return nil, fmt.Errorf("client is nil in ClusterExtensionAnalyzer")
|
||||
}
|
||||
if a.Client.GetDynamicClient() == nil {
|
||||
return nil, fmt.Errorf("dynamic client is nil in ClusterExtensionAnalyzer")
|
||||
}
|
||||
|
||||
list, err := a.Client.GetDynamicClient().Resource(clusterExtensionGVR).Namespace("").List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
for _, item := range list.Items {
|
||||
var failures []common.Failure
|
||||
extension, err := ConvertToClusterExtension(&item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("ClusterExtension: %s | Source: %s\n", extension.Name, extension.Spec.Source.Catalog.PackageName)
|
||||
failures, err = ValidateClusterExtension(failures, extension)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[extension.Name] = common.PreAnalysis{
|
||||
Extension: *extension,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, extension.Name, "").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.Node.ObjectMeta)
|
||||
if found {
|
||||
currentAnalysis.ParentObject = parent
|
||||
}
|
||||
a.Results = append(a.Results, currentAnalysis)
|
||||
}
|
||||
|
||||
return a.Results, err
|
||||
}
|
||||
|
||||
func ConvertToClusterExtension(u *unstructured.Unstructured) (*common.ClusterExtension, error) {
|
||||
var ce common.ClusterExtension
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &ce)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert to ClusterExtension: %w", err)
|
||||
}
|
||||
return &ce, nil
|
||||
}
|
||||
|
||||
func addExtensionConditionFailure(failures []common.Failure, extensionName string, extensionCondition metav1.Condition) []common.Failure {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("OLMv1 ClusterExtension: %s has condition of type %s, reason %s: %s", extensionName, extensionCondition.Type, extensionCondition.Reason, extensionCondition.Message),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: extensionName,
|
||||
Masked: util.MaskString(extensionName),
|
||||
},
|
||||
},
|
||||
})
|
||||
return failures
|
||||
}
|
||||
|
||||
func addExtensionFailure(failures []common.Failure, extensionName string, err error) []common.Failure {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("%s has error: %s", extensionName, err.Error()),
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: extensionName,
|
||||
Masked: util.MaskString(extensionName),
|
||||
},
|
||||
},
|
||||
})
|
||||
return failures
|
||||
}
|
||||
|
||||
func ValidateClusterExtension(failures []common.Failure, extension *common.ClusterExtension) ([]common.Failure, error) {
|
||||
if extension.Spec.Source.Catalog != nil && extension.Spec.Source.Catalog.UpgradeConstraintPolicy != "CatalogProvided" && extension.Spec.Source.Catalog.UpgradeConstraintPolicy != "SelfCertified" {
|
||||
failures = addExtensionFailure(failures, extension.Name, fmt.Errorf("invalid or missing extension.Spec.Source.Catalog.UpgradeConstraintPolicy (expecting 'SelfCertified' or 'CatalogProvided')"))
|
||||
}
|
||||
|
||||
if extension.Spec.Source.SourceType != "Catalog" {
|
||||
failures = addExtensionFailure(failures, extension.Name, fmt.Errorf("invalid or missing spec.source.sourceType (expecting 'Catalog')"))
|
||||
}
|
||||
|
||||
for _, condition := range extension.Status.Conditions {
|
||||
if condition.Status != "True" && condition.Type == "Installed" {
|
||||
failures = addExtensionConditionFailure(failures, extension.Name, condition)
|
||||
}
|
||||
if condition.Type == "Progressing" && condition.Reason != "Succeeded" {
|
||||
failures = addExtensionConditionFailure(failures, extension.Name, condition)
|
||||
}
|
||||
}
|
||||
|
||||
return failures, nil
|
||||
}
|
||||
179
pkg/analyzer/clusterextension_test.go
Normal file
179
pkg/analyzer/clusterextension_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
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"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestClusterExtensionAnalyzer(t *testing.T) {
|
||||
gvr := schema.GroupVersionResource{
|
||||
Group: "olm.operatorframework.io",
|
||||
Version: "v1",
|
||||
Resource: "clusterextensions",
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
|
||||
dynamicClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
|
||||
scheme,
|
||||
map[schema.GroupVersionResource]string{
|
||||
gvr: "ClusterExtensionList",
|
||||
},
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "olm.operatorframework.io/v1",
|
||||
"kind": "ClusterExtension",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "Valid SelfCertified ClusterExtension",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"source": map[string]interface{}{
|
||||
"sourceType": "Catalog",
|
||||
"catalog": map[string]interface{}{
|
||||
"upgradeConstraintPolicy": "SelfCertified",
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Installed",
|
||||
"status": "True",
|
||||
"reason": "Succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "olm.operatorframework.io/v1",
|
||||
"kind": "ClusterExtension",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "Valid CatalogProvided ClusterExtension",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"source": map[string]interface{}{
|
||||
"sourceType": "Catalog",
|
||||
"catalog": map[string]interface{}{
|
||||
"upgradeConstraintPolicy": "CatalogProvided",
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Installed",
|
||||
"status": "True",
|
||||
"reason": "Succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "olm.operatorframework.io/v1",
|
||||
"kind": "ClusterExtension",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "Invalid UpgradeConstraintPolicy",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"source": map[string]interface{}{
|
||||
"sourceType": "Catalog",
|
||||
"catalog": map[string]interface{}{
|
||||
"upgradeConstraintPolicy": "InvalidPolicy",
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Progressing",
|
||||
"status": "True",
|
||||
"reason": "Retrying",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "Installed",
|
||||
"status": "False",
|
||||
"reason": "Failed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "olm.operatorframework.io/v1",
|
||||
"kind": "ClusterExtension",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "Invalid SourceType",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"source": map[string]interface{}{
|
||||
"sourceType": "Git",
|
||||
"catalog": map[string]interface{}{
|
||||
"upgradeConstraintPolicy": "CatalogProvided",
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"conditions": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "Progressing",
|
||||
"status": "True",
|
||||
"reason": "Retrying",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"type": "Installed",
|
||||
"status": "False",
|
||||
"reason": "Failed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(),
|
||||
DynamicClient: dynamicClient,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "test",
|
||||
}
|
||||
|
||||
ceAnalyzer := ClusterExtensionAnalyzer{}
|
||||
results, err := ceAnalyzer.Analyze(config)
|
||||
for _, res := range results {
|
||||
fmt.Printf("Result: %s | Failures: %d\n", res.Name, len(res.Error))
|
||||
for _, err := range res.Error {
|
||||
fmt.Printf(" - %s\n", err)
|
||||
}
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(results))
|
||||
}
|
||||
82
pkg/analyzer/clusterserviceversion.go
Normal file
82
pkg/analyzer/clusterserviceversion.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type ClusterServiceVersionAnalyzer struct{}
|
||||
|
||||
var csvGVR = schema.GroupVersionResource{
|
||||
Group: "operators.coreos.com", Version: "v1alpha1", Resource: "clusterserviceversions",
|
||||
}
|
||||
|
||||
func (ClusterServiceVersionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
kind := "ClusterServiceVersion"
|
||||
|
||||
if a.Client.GetDynamicClient() == nil {
|
||||
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
|
||||
}
|
||||
|
||||
list, err := a.Client.GetDynamicClient().
|
||||
Resource(csvGVR).Namespace(metav1.NamespaceAll).
|
||||
List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []common.Result
|
||||
for _, item := range list.Items {
|
||||
ns := item.GetNamespace()
|
||||
name := item.GetName()
|
||||
phase, _, _ := unstructured.NestedString(item.Object, "status", "phase")
|
||||
|
||||
var failures []common.Failure
|
||||
if phase != "" && phase != "Succeeded" {
|
||||
// Superfície de condições para contexto
|
||||
if conds, _, _ := unstructured.NestedSlice(item.Object, "status", "conditions"); len(conds) > 0 {
|
||||
if msg := pickWorstCondition(conds); msg != "" {
|
||||
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q: %s", phase, msg)})
|
||||
}
|
||||
} else {
|
||||
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q (see status.conditions)", phase)})
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: kind,
|
||||
Name: ns + "/" + name,
|
||||
Error: failures,
|
||||
})
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// reaproveitamos o heurístico já usado em outros pontos
|
||||
func pickWorstCondition(conds []interface{}) string {
|
||||
for _, c := range conds {
|
||||
m, ok := c.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if s, _ := m["status"].(string); s == "True" {
|
||||
continue
|
||||
}
|
||||
r, _ := m["reason"].(string)
|
||||
msg, _ := m["message"].(string)
|
||||
if r == "" && msg == "" {
|
||||
continue
|
||||
}
|
||||
if r != "" && msg != "" {
|
||||
return r + ": " + msg
|
||||
}
|
||||
return r + msg
|
||||
}
|
||||
return ""
|
||||
}
|
||||
78
pkg/analyzer/clusterserviceversion_test.go
Normal file
78
pkg/analyzer/clusterserviceversion_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
)
|
||||
|
||||
func TestClusterServiceVersionAnalyzer(t *testing.T) {
|
||||
ok := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1alpha1",
|
||||
"kind": "ClusterServiceVersion",
|
||||
"metadata": map[string]any{
|
||||
"name": "ok",
|
||||
"namespace": "ns1",
|
||||
},
|
||||
"status": map[string]any{"phase": "Succeeded"},
|
||||
},
|
||||
}
|
||||
|
||||
bad := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1alpha1",
|
||||
"kind": "ClusterServiceVersion",
|
||||
"metadata": map[string]any{
|
||||
"name": "bad",
|
||||
"namespace": "ns1",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"phase": "Failed",
|
||||
// IMPORTANT: conditions must be []interface{}, not []map[string]any
|
||||
"conditions": []interface{}{
|
||||
map[string]any{
|
||||
"status": "False",
|
||||
"reason": "ErrorResolving",
|
||||
"message": "missing dep",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
listKinds := map[schema.GroupVersionResource]string{
|
||||
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "clusterserviceversions"}: "ClusterServiceVersionList",
|
||||
}
|
||||
|
||||
// Use a non-nil scheme with dynamicfake
|
||||
scheme := runtime.NewScheme()
|
||||
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ok, bad)
|
||||
|
||||
a := common.Analyzer{
|
||||
Context: context.TODO(),
|
||||
Client: &kubernetes.Client{DynamicClient: dc},
|
||||
}
|
||||
|
||||
res, err := (ClusterServiceVersionAnalyzer{}).Analyze(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Analyze error: %v", err)
|
||||
}
|
||||
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(res))
|
||||
}
|
||||
if res[0].Kind != "ClusterServiceVersion" || !strings.Contains(res[0].Name, "ns1/bad") {
|
||||
t.Fatalf("unexpected result: %#v", res[0])
|
||||
}
|
||||
if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "missing dep") {
|
||||
t.Fatalf("expected 'missing dep' in failure, got %#v", res[0].Error)
|
||||
}
|
||||
}
|
||||
125
pkg/analyzer/configmap.go
Normal file
125
pkg/analyzer/configmap.go
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type ConfigMapAnalyzer struct{}
|
||||
|
||||
func (ConfigMapAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
kind := "ConfigMap"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
// Get all ConfigMaps in the namespace
|
||||
configMaps, err := a.Client.GetClient().CoreV1().ConfigMaps(a.Namespace).List(a.Context, metav1.ListOptions{
|
||||
LabelSelector: a.LabelSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all Pods to check ConfigMap usage
|
||||
pods, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []common.Result
|
||||
|
||||
// Track which ConfigMaps are used
|
||||
usedConfigMaps := make(map[string]bool)
|
||||
configMapUsage := make(map[string][]string) // maps ConfigMap name to list of pods using it
|
||||
|
||||
// Analyze ConfigMap usage in Pods
|
||||
for _, pod := range pods.Items {
|
||||
// Check volume mounts
|
||||
for _, volume := range pod.Spec.Volumes {
|
||||
if volume.ConfigMap != nil {
|
||||
usedConfigMaps[volume.ConfigMap.Name] = true
|
||||
configMapUsage[volume.ConfigMap.Name] = append(configMapUsage[volume.ConfigMap.Name], pod.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Check environment variables
|
||||
for _, container := range pod.Spec.Containers {
|
||||
for _, env := range container.EnvFrom {
|
||||
if env.ConfigMapRef != nil {
|
||||
usedConfigMaps[env.ConfigMapRef.Name] = true
|
||||
configMapUsage[env.ConfigMapRef.Name] = append(configMapUsage[env.ConfigMapRef.Name], pod.Name)
|
||||
}
|
||||
}
|
||||
for _, env := range container.Env {
|
||||
if env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil {
|
||||
usedConfigMaps[env.ValueFrom.ConfigMapKeyRef.Name] = true
|
||||
configMapUsage[env.ValueFrom.ConfigMapKeyRef.Name] = append(configMapUsage[env.ValueFrom.ConfigMapKeyRef.Name], pod.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze each ConfigMap
|
||||
for _, cm := range configMaps.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for unused ConfigMaps
|
||||
if !usedConfigMaps[cm.Name] {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("ConfigMap %s is not used by any pods in the namespace", cm.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
|
||||
// Check for empty ConfigMaps
|
||||
if len(cm.Data) == 0 && len(cm.BinaryData) == 0 {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("ConfigMap %s is empty", cm.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
|
||||
// Check for large ConfigMaps (over 1MB)
|
||||
totalSize := 0
|
||||
for _, value := range cm.Data {
|
||||
totalSize += len(value)
|
||||
}
|
||||
for _, value := range cm.BinaryData {
|
||||
totalSize += len(value)
|
||||
}
|
||||
if totalSize > 1024*1024 { // 1MB
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("ConfigMap %s is larger than 1MB (%d bytes)", cm.Name, totalSize),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: kind,
|
||||
Name: fmt.Sprintf("%s/%s", cm.Namespace, cm.Name),
|
||||
Error: failures,
|
||||
})
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, cm.Name, cm.Namespace).Set(float64(len(failures)))
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
149
pkg/analyzer/configmap_test.go
Normal file
149
pkg/analyzer/configmap_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestConfigMapAnalyzer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
configMaps []v1.ConfigMap
|
||||
pods []v1.Pod
|
||||
expectedErrors int
|
||||
}{
|
||||
{
|
||||
name: "unused configmap",
|
||||
namespace: "default",
|
||||
configMaps: []v1.ConfigMap{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "unused-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "empty configmap",
|
||||
namespace: "default",
|
||||
configMaps: []v1.ConfigMap{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "empty-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "large configmap",
|
||||
namespace: "default",
|
||||
configMaps: []v1.ConfigMap{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "large-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": string(make([]byte, 1024*1024+1)), // 1MB + 1 byte
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "used configmap",
|
||||
namespace: "default",
|
||||
configMaps: []v1.ConfigMap{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "used-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
pods: []v1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pod",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "test-container",
|
||||
EnvFrom: []v1.EnvFromSource{
|
||||
{
|
||||
ConfigMapRef: &v1.ConfigMapEnvSource{
|
||||
LocalObjectReference: v1.LocalObjectReference{
|
||||
Name: "used-cm",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
// Create test resources
|
||||
for _, cm := range tt.configMaps {
|
||||
_, err := client.CoreV1().ConfigMaps(tt.namespace).Create(context.TODO(), &cm, metav1.CreateOptions{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
for _, pod := range tt.pods {
|
||||
_, err := client.CoreV1().Pods(tt.namespace).Create(context.TODO(), &pod, metav1.CreateOptions{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
analyzer := ConfigMapAnalyzer{}
|
||||
results, err := analyzer.Analyze(common.Analyzer{
|
||||
Client: &kubernetes.Client{Client: client},
|
||||
Context: context.TODO(),
|
||||
Namespace: tt.namespace,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedErrors, len(results))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -22,179 +22,274 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"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 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: 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.CronJobSpec{
|
||||
Schedule: "*/1 * * * *",
|
||||
ConcurrencyPolicy: "Allow",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
tests := []struct {
|
||||
name string
|
||||
config common.Analyzer
|
||||
expectations []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "Suspended CronJob",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
Name: "suspended-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "example-container",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
RestartPolicy: v1.RestartPolicyOnFailure,
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *",
|
||||
Suspend: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/suspended-job",
|
||||
failuresCount: 1, // One failure for being suspended
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid schedule format",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "invalid-schedule",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "invalid-cron", // Invalid cron format
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/invalid-schedule",
|
||||
failuresCount: 1, // One failure for invalid schedule
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Negative starting deadline",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "negative-deadline",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *",
|
||||
StartingDeadlineSeconds: int64Ptr(-60), // Negative deadline
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/negative-deadline",
|
||||
failuresCount: 1, // One failure for negative deadline
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid CronJob",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "valid-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *", // Valid cron format
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
// No expectations for valid job
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple issues",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "multiple-issues",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "invalid-cron",
|
||||
StartingDeadlineSeconds: int64Ptr(-60),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/multiple-issues",
|
||||
failuresCount: 2, // Two failures: invalid schedule and negative deadline
|
||||
},
|
||||
},
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
cjAnalyzer := CronJobAnalyzer{}
|
||||
results, err := cjAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
analyzer := CronJobAnalyzer{}
|
||||
results, err := analyzer.Analyze(tt.config)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, len(tt.expectations))
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
// Sort results by name for consistent comparison
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []string{
|
||||
"default/CJ2",
|
||||
"default/CJ3",
|
||||
"default/CJ4",
|
||||
}
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i], result.Name)
|
||||
for i, expectation := range tt.expectations {
|
||||
require.Equal(t, expectation.name, results[i].Name)
|
||||
require.Len(t, results[i].Error, expectation.failuresCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronJobAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
suspend := new(bool)
|
||||
*suspend = true
|
||||
|
||||
invalidStartingDeadline := new(int64)
|
||||
*invalidStartingDeadline = -7
|
||||
|
||||
validStartingDeadline := new(int64)
|
||||
*validStartingDeadline = 7
|
||||
func TestCronJobAnalyzerLabelSelector(t *testing.T) {
|
||||
clientSet := fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "job-with-label",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "invalid-cron", // This should trigger a failure
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "job-without-label",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "invalid-cron", // This should trigger a failure
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Test with label selector
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "cronjob",
|
||||
},
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
),
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=cronjob",
|
||||
LabelSelector: "app=test",
|
||||
}
|
||||
|
||||
cjAnalyzer := CronJobAnalyzer{}
|
||||
results, err := cjAnalyzer.Analyze(config)
|
||||
analyzer := CronJobAnalyzer{}
|
||||
results, err := analyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/CJ1", results[0].Name)
|
||||
require.Equal(t, "default/job-with-label", results[0].Name)
|
||||
}
|
||||
|
||||
func TestCheckCronScheduleIsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedule string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid schedule - every 5 minutes",
|
||||
schedule: "*/5 * * * *",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid schedule - specific time",
|
||||
schedule: "0 2 * * *",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid schedule - complex",
|
||||
schedule: "0 0 1,15 * 3",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid schedule - wrong format",
|
||||
schedule: "invalid-cron",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid schedule - too many fields",
|
||||
schedule: "* * * * * *",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid schedule - empty string",
|
||||
schedule: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := CheckCronScheduleIsValid(tt.schedule)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,22 +54,41 @@ func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
|
||||
|
||||
for _, deployment := range deployments.Items {
|
||||
var failures []common.Failure
|
||||
if *deployment.Spec.Replicas != deployment.Status.Replicas {
|
||||
doc := apiDoc.GetApiDocV2("spec.replicas")
|
||||
if *deployment.Spec.Replicas != deployment.Status.ReadyReplicas {
|
||||
if deployment.Status.Replicas > *deployment.Spec.Replicas {
|
||||
doc := apiDoc.GetApiDocV2("spec.replicas")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Deployment %s/%s has %d replicas but %d are available", deployment.Namespace, deployment.Name, *deployment.Spec.Replicas, deployment.Status.Replicas),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: deployment.Namespace,
|
||||
Masked: util.MaskString(deployment.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: deployment.Name,
|
||||
Masked: util.MaskString(deployment.Name),
|
||||
},
|
||||
}})
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Deployment %s/%s has %d replicas in spec but %d replicas in status because status field is not updated yet after scaling and %d replicas are available with status running", deployment.Namespace, deployment.Name, *deployment.Spec.Replicas, deployment.Status.Replicas, deployment.Status.ReadyReplicas),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: deployment.Namespace,
|
||||
Masked: util.MaskString(deployment.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: deployment.Name,
|
||||
Masked: util.MaskString(deployment.Name),
|
||||
},
|
||||
}})
|
||||
|
||||
} else {
|
||||
doc := apiDoc.GetApiDocV2("spec.replicas")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Deployment %s/%s has %d replicas but %d are available with status running", deployment.Namespace, deployment.Name, *deployment.Spec.Replicas, deployment.Status.ReadyReplicas),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: deployment.Namespace,
|
||||
Masked: util.MaskString(deployment.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: deployment.Name,
|
||||
Masked: util.MaskString(deployment.Name),
|
||||
},
|
||||
}})
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", deployment.Namespace, deployment.Name)] = common.PreAnalysis{
|
||||
|
||||
137
pkg/analyzer/events_test.go
Normal file
137
pkg/analyzer/events_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package analyzer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func FetchLatestEvent(ctx context.Context, client kubernetes.Interface, namespace, eventName string) (*v1.Event, error) {
|
||||
// List events in the specified namespace
|
||||
events, err := client.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var latestEvent *v1.Event
|
||||
for _, event := range events.Items {
|
||||
// Check if the event name matches the requested name (partial match)
|
||||
if eventName == "" || event.Name == eventName {
|
||||
if latestEvent == nil || event.LastTimestamp.Time.After(latestEvent.LastTimestamp.Time) {
|
||||
latestEvent = &event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no matching event is found, return an error
|
||||
if latestEvent == nil {
|
||||
return nil, errors.New("no matching events found")
|
||||
}
|
||||
return latestEvent, nil
|
||||
}
|
||||
func TestFetchLatestEvent(t *testing.T) {
|
||||
fakeClient := fake.NewSimpleClientset()
|
||||
|
||||
// Simulating events with different timestamps
|
||||
event1 := &v1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-event-1",
|
||||
Namespace: "default",
|
||||
},
|
||||
LastTimestamp: metav1.Time{Time: time.Now()},
|
||||
}
|
||||
event2 := &v1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-event-2",
|
||||
Namespace: "default",
|
||||
},
|
||||
LastTimestamp: metav1.Time{Time: time.Now().Add(-time.Hour)}, // event1 should be fetched as it's newer
|
||||
}
|
||||
|
||||
// ✅ Explicitly ensure namespace exists
|
||||
_, err := fakeClient.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "default"},
|
||||
}, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create namespace: %v", err)
|
||||
}
|
||||
|
||||
// ✅ Ensure events are properly created and stored in the fake client
|
||||
_, err = fakeClient.CoreV1().Events("default").Create(context.TODO(), event1, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create event1: %v", err)
|
||||
}
|
||||
|
||||
_, err = fakeClient.CoreV1().Events("default").Create(context.TODO(), event2, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create event2: %v", err)
|
||||
}
|
||||
|
||||
// 🔍 Debug: Check if events exist before running FetchLatestEvent
|
||||
storedEvents, _ := fakeClient.CoreV1().Events("default").List(context.TODO(), metav1.ListOptions{})
|
||||
if len(storedEvents.Items) == 0 {
|
||||
t.Fatal("No events were found in the fake client. Ensure event creation is working correctly.")
|
||||
}
|
||||
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
nameToFind string
|
||||
expected *v1.Event
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
name: "Valid case - fetch the latest event",
|
||||
namespace: "default",
|
||||
nameToFind: "test-event-1", // Match exact event name
|
||||
expected: event1, // event1 has the latest timestamp
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
name: "Nonexistent event",
|
||||
namespace: "default",
|
||||
nameToFind: "nonexistent-event", // Should not exist
|
||||
expected: nil,
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
name: "Nonexistent namespace",
|
||||
namespace: "nonexistent-namespace", // Namespace doesn't exist
|
||||
nameToFind: "test-event",
|
||||
expected: nil,
|
||||
shouldFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Call the function to fetch the latest event
|
||||
event, err := FetchLatestEvent(context.TODO(), fakeClient, tt.namespace, tt.nameToFind)
|
||||
|
||||
// Handle the expected outcomes based on the test case
|
||||
if tt.shouldFail {
|
||||
if err == nil {
|
||||
t.Error("Expected an error, but got nil")
|
||||
}
|
||||
if event != nil {
|
||||
t.Errorf("Expected nil event, but got event: %s", event.Name)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, but got %v", err)
|
||||
}
|
||||
if event != nil && event.Name != tt.expected.Name {
|
||||
t.Errorf("Expected event name %s, got %s", tt.expected.Name, event.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -34,7 +35,7 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "autoscaling",
|
||||
Version: "v1",
|
||||
Version: "v2",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
@@ -53,15 +54,25 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
for _, hpa := range list.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
|
||||
//check the error from status field
|
||||
conditions := hpa.Status.Conditions
|
||||
for _, condition := range conditions {
|
||||
if condition.Status != "True" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: condition.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
// https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#appendix-horizontal-pod-autoscaler-status-conditions
|
||||
switch condition.Type {
|
||||
case autoscalingv2.ScalingLimited:
|
||||
if condition.Status == corev1.ConditionTrue {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: condition.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
default:
|
||||
if condition.Status == corev1.ConditionFalse {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: condition.Message,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -566,7 +566,6 @@ func TestHPAAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
}
|
||||
|
||||
|
||||
func TestHPAAnalyzerStatusFieldAbleToScale(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
@@ -584,8 +583,8 @@ func TestHPAAnalyzerStatusFieldAbleToScale(t *testing.T) {
|
||||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
|
||||
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
|
||||
{
|
||||
Type: "AbleToScale",
|
||||
Status: "False",
|
||||
Type: "AbleToScale",
|
||||
Status: "False",
|
||||
Message: "test reason",
|
||||
},
|
||||
},
|
||||
@@ -607,7 +606,6 @@ func TestHPAAnalyzerStatusFieldAbleToScale(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
func TestHPAAnalyzerStatusFieldScalingActive(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
@@ -625,8 +623,8 @@ func TestHPAAnalyzerStatusFieldScalingActive(t *testing.T) {
|
||||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
|
||||
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
|
||||
{
|
||||
Type: autoscalingv2.ScalingActive,
|
||||
Status: "False",
|
||||
Type: autoscalingv2.ScalingActive,
|
||||
Status: "False",
|
||||
Message: "test reason",
|
||||
},
|
||||
},
|
||||
@@ -648,8 +646,6 @@ func TestHPAAnalyzerStatusFieldScalingActive(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
func TestHPAAnalyzerStatusFieldScalingLimited(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
@@ -667,8 +663,8 @@ func TestHPAAnalyzerStatusFieldScalingLimited(t *testing.T) {
|
||||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
|
||||
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
|
||||
{
|
||||
Type: autoscalingv2.ScalingLimited,
|
||||
Status: "False",
|
||||
Type: autoscalingv2.ScalingLimited,
|
||||
Status: "False",
|
||||
Message: "test reason",
|
||||
},
|
||||
},
|
||||
@@ -690,7 +686,6 @@ func TestHPAAnalyzerStatusFieldScalingLimited(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
func TestHPAAnalyzerStatusField(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
@@ -708,18 +703,18 @@ func TestHPAAnalyzerStatusField(t *testing.T) {
|
||||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
|
||||
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
|
||||
{
|
||||
Type: autoscalingv2.AbleToScale,
|
||||
Status: "True",
|
||||
Type: autoscalingv2.AbleToScale,
|
||||
Status: "True",
|
||||
Message: "recommended size matches current size",
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ScalingActive,
|
||||
Status: "True",
|
||||
Type: autoscalingv2.ScalingActive,
|
||||
Status: "True",
|
||||
Message: "the HPA was able to successfully calculate a replica count",
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ScalingLimited,
|
||||
Status: "True",
|
||||
Type: autoscalingv2.ScalingLimited,
|
||||
Status: "True",
|
||||
Message: "the desired replica count is less than the minimum replica count",
|
||||
},
|
||||
},
|
||||
@@ -739,4 +734,88 @@ func TestHPAAnalyzerStatusField(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, len(analysisResults), 1)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestHPAAnalyzerStatusScalingLimitedError(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
Kind: "Deployment",
|
||||
Name: "example",
|
||||
},
|
||||
},
|
||||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
|
||||
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
|
||||
{
|
||||
Type: autoscalingv2.AbleToScale,
|
||||
Status: "True",
|
||||
Message: "recommended size matches current size",
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ScalingActive,
|
||||
Status: "True",
|
||||
Message: "the HPA was able to successfully calculate a replica count",
|
||||
},
|
||||
{
|
||||
Type: autoscalingv2.ScalingLimited,
|
||||
Status: "True",
|
||||
Message: "the desired replica count is less than the minimum replica count",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "example",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
hpaAnalyzer := HpaAnalyzer{}
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
analysisResults, err := hpaAnalyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
var errorFound bool
|
||||
want := "the desired replica count is less than the minimum replica count"
|
||||
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 HorizontalPodAutoscaler's analysis results", want)
|
||||
}
|
||||
}
|
||||
@@ -15,226 +15,243 @@ package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/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) {
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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: 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",
|
||||
},
|
||||
|
||||
// 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",
|
||||
}
|
||||
|
||||
igAnalyzer := IngressAnalyzer{}
|
||||
results, err := igAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
// Create test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
ingress *networkingv1.Ingress
|
||||
expectedIssues []string
|
||||
}{
|
||||
{
|
||||
name: "default/Ingress1",
|
||||
failuresCount: 1,
|
||||
name: "Non-existent backend service",
|
||||
ingress: &networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ingress",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
Rules: []networkingv1.IngressRule{
|
||||
{
|
||||
Host: "example.com",
|
||||
IngressRuleValue: networkingv1.IngressRuleValue{
|
||||
HTTP: &networkingv1.HTTPIngressRuleValue{
|
||||
Paths: []networkingv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/",
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
Name: "non-existent-service",
|
||||
Port: networkingv1.ServiceBackendPort{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedIssues: []string{
|
||||
"Ingress default/test-ingress does not specify an Ingress class.",
|
||||
"Ingress uses the service default/non-existent-service which does not exist.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default/Ingress2",
|
||||
failuresCount: 1,
|
||||
name: "Non-existent TLS secret",
|
||||
ingress: &networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ingress-tls",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
TLS: []networkingv1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"example.com"},
|
||||
SecretName: "non-existent-secret",
|
||||
},
|
||||
},
|
||||
Rules: []networkingv1.IngressRule{
|
||||
{
|
||||
Host: "example.com",
|
||||
IngressRuleValue: networkingv1.IngressRuleValue{
|
||||
HTTP: &networkingv1.HTTPIngressRuleValue{
|
||||
Paths: []networkingv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/",
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
Name: "test-service",
|
||||
Port: networkingv1.ServiceBackendPort{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedIssues: []string{
|
||||
"Ingress default/test-ingress-tls does not specify an Ingress class.",
|
||||
"Ingress uses the service default/test-service which does not exist.",
|
||||
"Ingress uses the secret default/non-existent-secret as a TLS certificate which does not exist.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default/Ingress5",
|
||||
failuresCount: 4,
|
||||
name: "Multiple issues",
|
||||
ingress: &networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ingress-multi",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
TLS: []networkingv1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"example.com"},
|
||||
SecretName: "non-existent-secret",
|
||||
},
|
||||
},
|
||||
Rules: []networkingv1.IngressRule{
|
||||
{
|
||||
Host: "example.com",
|
||||
IngressRuleValue: networkingv1.IngressRuleValue{
|
||||
HTTP: &networkingv1.HTTPIngressRuleValue{
|
||||
Paths: []networkingv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/",
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
Name: "non-existent-service",
|
||||
Port: networkingv1.ServiceBackendPort{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedIssues: []string{
|
||||
"Ingress default/test-ingress-multi does not specify an Ingress class.",
|
||||
"Ingress uses the service default/non-existent-service which does not exist.",
|
||||
"Ingress uses the secret default/non-existent-secret as a TLS certificate which does not exist.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
// Run test cases
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create a new context and clientset for each test case
|
||||
ctx := context.Background()
|
||||
clientset := fake.NewSimpleClientset()
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
// Create the ingress in the fake clientset
|
||||
_, err := clientset.NetworkingV1().Ingresses(tc.ingress.Namespace).Create(ctx, tc.ingress, metav1.CreateOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create the analyzer configuration
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
Context: ctx,
|
||||
Namespace: tc.ingress.Namespace,
|
||||
}
|
||||
|
||||
// Create the analyzer and run analysis
|
||||
analyzer := IngressAnalyzer{}
|
||||
results, err := analyzer.Analyze(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that we got the expected number of issues
|
||||
assert.Len(t, results, 1, "Expected 1 result")
|
||||
result := results[0]
|
||||
assert.Len(t, result.Error, len(tc.expectedIssues), "Expected %d issues, got %d", len(tc.expectedIssues), len(result.Error))
|
||||
|
||||
// Check that each expected issue is present
|
||||
for _, expectedIssue := range tc.expectedIssues {
|
||||
found := false
|
||||
for _, failure := range result.Error {
|
||||
if failure.Text == expectedIssue {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "Expected to find issue: %s", expectedIssue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
validIgClassName := new(string)
|
||||
*validIgClassName = "valid-ingress-class"
|
||||
func TestIngressAnalyzerLabelSelector(t *testing.T) {
|
||||
clientSet := fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ingress-with-label",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
// Missing ingress class to trigger a failure
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ingress-without-label",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
// Missing ingress class to trigger a failure
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Test with label selector
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "ingress",
|
||||
},
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
),
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=ingress",
|
||||
LabelSelector: "app=test",
|
||||
}
|
||||
|
||||
igAnalyzer := IngressAnalyzer{}
|
||||
results, err := igAnalyzer.Analyze(config)
|
||||
analyzer := IngressAnalyzer{}
|
||||
results, err := analyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/Ingress1", results[0].Name)
|
||||
|
||||
require.Equal(t, "default/ingress-with-label", results[0].Name)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func pathTypePtr(p networkingv1.PathType) *networkingv1.PathType {
|
||||
return &p
|
||||
}
|
||||
|
||||
75
pkg/analyzer/installplan_test.go
Normal file
75
pkg/analyzer/installplan_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
)
|
||||
|
||||
func TestInstallPlanAnalyzer(t *testing.T) {
|
||||
ok := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1alpha1",
|
||||
"kind": "InstallPlan",
|
||||
"metadata": map[string]any{
|
||||
"name": "ip-ok",
|
||||
"namespace": "ns1",
|
||||
},
|
||||
"status": map[string]any{"phase": "Complete"},
|
||||
},
|
||||
}
|
||||
|
||||
bad := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1alpha1",
|
||||
"kind": "InstallPlan",
|
||||
"metadata": map[string]any{
|
||||
"name": "ip-bad",
|
||||
"namespace": "ns1",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"phase": "Failed",
|
||||
"conditions": []interface{}{
|
||||
map[string]any{
|
||||
"reason": "ExecutionError",
|
||||
"message": "something went wrong",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
listKinds := map[schema.GroupVersionResource]string{
|
||||
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "installplans"}: "InstallPlanList",
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ok, bad)
|
||||
|
||||
a := common.Analyzer{
|
||||
Context: context.TODO(),
|
||||
Client: &kubernetes.Client{DynamicClient: dc},
|
||||
}
|
||||
|
||||
res, err := (InstallPlanAnalyzer{}).Analyze(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Analyze error: %v", err)
|
||||
}
|
||||
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(res))
|
||||
}
|
||||
if res[0].Kind != "InstallPlan" || !strings.Contains(res[0].Name, "ns1/ip-bad") {
|
||||
t.Fatalf("unexpected result: %#v", res[0])
|
||||
}
|
||||
if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "ExecutionError") {
|
||||
t.Fatalf("expected 'ExecutionError' in failure, got %#v", res[0].Error)
|
||||
}
|
||||
}
|
||||
72
pkg/analyzer/instalplan.go
Normal file
72
pkg/analyzer/instalplan.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type InstallPlanAnalyzer struct{}
|
||||
|
||||
var ipGVR = schema.GroupVersionResource{
|
||||
Group: "operators.coreos.com", Version: "v1alpha1", Resource: "installplans",
|
||||
}
|
||||
|
||||
func (InstallPlanAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
kind := "InstallPlan"
|
||||
if a.Client.GetDynamicClient() == nil {
|
||||
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
|
||||
}
|
||||
|
||||
list, err := a.Client.GetDynamicClient().
|
||||
Resource(ipGVR).Namespace(metav1.NamespaceAll).
|
||||
List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []common.Result
|
||||
for _, item := range list.Items {
|
||||
ns, name := item.GetNamespace(), item.GetName()
|
||||
phase, _, _ := unstructured.NestedString(item.Object, "status", "phase")
|
||||
|
||||
var failures []common.Failure
|
||||
if phase != "" && phase != "Complete" {
|
||||
reason := firstCondStr(&item, "reason")
|
||||
msg := firstCondStr(&item, "message")
|
||||
switch {
|
||||
case reason != "" && msg != "":
|
||||
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q: %s: %s", phase, reason, msg)})
|
||||
case reason != "" || msg != "":
|
||||
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q: %s%s", phase, reason, msg)})
|
||||
default:
|
||||
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q (approval/manual? check status.conditions)", phase)})
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: kind,
|
||||
Name: ns + "/" + name,
|
||||
Error: failures,
|
||||
})
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func firstCondStr(u *unstructured.Unstructured, field string) string {
|
||||
conds, _, _ := unstructured.NestedSlice(u.Object, "status", "conditions")
|
||||
if len(conds) == 0 {
|
||||
return ""
|
||||
}
|
||||
m, _ := conds[0].(map[string]any)
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
v, _ := m[field].(string)
|
||||
return v
|
||||
}
|
||||
107
pkg/analyzer/job.go
Normal file
107
pkg/analyzer/job.go
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
Copyright 2025 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/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type JobAnalyzer struct{}
|
||||
|
||||
func (analyzer JobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "Job"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "batch",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
JobList, err := a.Client.GetClient().BatchV1().Jobs(a.Namespace).List(a.Context, v1.ListOptions{LabelSelector: a.LabelSelector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var preAnalysis = map[string]common.PreAnalysis{}
|
||||
|
||||
for _, Job := range JobList.Items {
|
||||
var failures []common.Failure
|
||||
if Job.Spec.Suspend != nil && *Job.Spec.Suspend {
|
||||
doc := apiDoc.GetApiDocV2("spec.suspend")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Job %s is suspended", Job.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: Job.Namespace,
|
||||
Masked: util.MaskString(Job.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: Job.Name,
|
||||
Masked: util.MaskString(Job.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if Job.Status.Failed > 0 {
|
||||
doc := apiDoc.GetApiDocV2("status.failed")
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Job %s has failed", Job.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: Job.Namespace,
|
||||
Masked: util.MaskString(Job.Namespace),
|
||||
},
|
||||
{
|
||||
Unmasked: Job.Name,
|
||||
Masked: util.MaskString(Job.Name),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s/%s", Job.Namespace, Job.Name)] = common.PreAnalysis{
|
||||
FailureDetails: failures,
|
||||
}
|
||||
AnalyzerErrorsMetric.WithLabelValues(kind, Job.Name, Job.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)
|
||||
}
|
||||
|
||||
return a.Results, nil
|
||||
}
|
||||
215
pkg/analyzer/job_test.go
Normal file
215
pkg/analyzer/job_test.go
Normal file
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
Copyright 2025 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"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestJobAnalyzer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config common.Analyzer
|
||||
expectations []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "Suspended Job",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "suspended-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Suspend: boolPtr(true),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/suspended-job",
|
||||
failuresCount: 1, // One failure for being suspended
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Failed Job",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "failed-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.JobSpec{},
|
||||
Status: batchv1.JobStatus{
|
||||
Failed: 1,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/failed-job",
|
||||
failuresCount: 1, // One failure for failed job
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid Job",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "valid-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.JobSpec{},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
// No expectations for valid job
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple issues",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "multiple-issues",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Suspend: boolPtr(true),
|
||||
},
|
||||
Status: batchv1.JobStatus{
|
||||
Failed: 1,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/multiple-issues",
|
||||
failuresCount: 2, // Two failures: suspended and failed job
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
analyzer := JobAnalyzer{}
|
||||
results, err := analyzer.Analyze(tt.config)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, len(tt.expectations))
|
||||
|
||||
// Sort results by name for consistent comparison
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
for i, expectation := range tt.expectations {
|
||||
require.Equal(t, expectation.name, results[i].Name)
|
||||
require.Len(t, results[i].Error, expectation.failuresCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobAnalyzerLabelSelector(t *testing.T) {
|
||||
clientSet := fake.NewSimpleClientset(
|
||||
&batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "job-with-label",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{},
|
||||
Status: batchv1.JobStatus{
|
||||
Failed: 1,
|
||||
},
|
||||
},
|
||||
&batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "job-without-label",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.JobSpec{},
|
||||
},
|
||||
)
|
||||
|
||||
// Test with label selector
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=test",
|
||||
}
|
||||
|
||||
analyzer := JobAnalyzer{}
|
||||
results, err := analyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/job-with-label", results[0].Name)
|
||||
}
|
||||
@@ -46,15 +46,17 @@ func (NodeAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
// https://kubernetes.io/docs/concepts/architecture/nodes/#condition
|
||||
switch nodeCondition.Type {
|
||||
case v1.NodeReady:
|
||||
if nodeCondition.Status == v1.ConditionTrue {
|
||||
break
|
||||
if nodeCondition.Status != v1.ConditionTrue {
|
||||
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
|
||||
}
|
||||
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
|
||||
// k3s `EtcdIsVoter`` should not be reported as an error
|
||||
case v1.NodeConditionType("EtcdIsVoter"):
|
||||
break
|
||||
default:
|
||||
if nodeCondition.Status != v1.ConditionFalse {
|
||||
// For other conditions:
|
||||
// - Report True or Unknown status as failures (for standard conditions)
|
||||
// - Report any unknown condition type as a failure
|
||||
if nodeCondition.Status == v1.ConditionTrue || nodeCondition.Status == v1.ConditionUnknown || !isKnownNodeConditionType(nodeCondition.Type) {
|
||||
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
|
||||
}
|
||||
}
|
||||
@@ -99,3 +101,17 @@ func addNodeConditionFailure(failures []common.Failure, nodeName string, nodeCon
|
||||
})
|
||||
return failures
|
||||
}
|
||||
|
||||
// isKnownNodeConditionType checks if the condition type is a standard Kubernetes node condition
|
||||
func isKnownNodeConditionType(conditionType v1.NodeConditionType) bool {
|
||||
switch conditionType {
|
||||
case v1.NodeReady,
|
||||
v1.NodeMemoryPressure,
|
||||
v1.NodeDiskPressure,
|
||||
v1.NodePIDPressure,
|
||||
v1.NodeNetworkUnavailable:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
46
pkg/analyzer/operatorgroup.go
Normal file
46
pkg/analyzer/operatorgroup.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type OperatorGroupAnalyzer struct{}
|
||||
|
||||
var ogGVR = schema.GroupVersionResource{
|
||||
Group: "operators.coreos.com", Version: "v1", Resource: "operatorgroups",
|
||||
}
|
||||
|
||||
func (OperatorGroupAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
kind := "OperatorGroup"
|
||||
if a.Client.GetDynamicClient() == nil {
|
||||
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
|
||||
}
|
||||
|
||||
list, err := a.Client.GetDynamicClient().
|
||||
Resource(ogGVR).Namespace(metav1.NamespaceAll).
|
||||
List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
countByNS := map[string]int{}
|
||||
for _, it := range list.Items {
|
||||
countByNS[it.GetNamespace()]++
|
||||
}
|
||||
|
||||
var results []common.Result
|
||||
for ns, n := range countByNS {
|
||||
if n > 1 {
|
||||
results = append(results, common.Result{
|
||||
Kind: kind,
|
||||
Name: ns,
|
||||
Error: []common.Failure{{Text: fmt.Sprintf("%d OperatorGroups in namespace; this can break CSV resolution", n)}},
|
||||
})
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
70
pkg/analyzer/operatorgroup_test.go
Normal file
70
pkg/analyzer/operatorgroup_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
)
|
||||
|
||||
func TestOperatorGroupAnalyzer(t *testing.T) {
|
||||
og1 := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1",
|
||||
"kind": "OperatorGroup",
|
||||
"metadata": map[string]any{
|
||||
"name": "og-1",
|
||||
"namespace": "ns-a",
|
||||
},
|
||||
},
|
||||
}
|
||||
og2 := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1",
|
||||
"kind": "OperatorGroup",
|
||||
"metadata": map[string]any{
|
||||
"name": "og-2",
|
||||
"namespace": "ns-a",
|
||||
},
|
||||
},
|
||||
}
|
||||
og3 := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1",
|
||||
"kind": "OperatorGroup",
|
||||
"metadata": map[string]any{
|
||||
"name": "og-3",
|
||||
"namespace": "ns-b",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
listKinds := map[schema.GroupVersionResource]string{
|
||||
{Group: "operators.coreos.com", Version: "v1", Resource: "operatorgroups"}: "OperatorGroupList",
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, og1, og2, og3)
|
||||
|
||||
a := common.Analyzer{
|
||||
Context: context.TODO(),
|
||||
Client: &kubernetes.Client{DynamicClient: dc},
|
||||
}
|
||||
|
||||
res, err := (OperatorGroupAnalyzer{}).Analyze(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Analyze error: %v", err)
|
||||
}
|
||||
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("expected 1 result for ns-a overlap, got %d", len(res))
|
||||
}
|
||||
if res[0].Kind != "OperatorGroup" || res[0].Name != "ns-a" {
|
||||
t.Fatalf("unexpected result: %#v", res[0])
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,20 @@ func analyzeContainerStatusFailures(a common.Analyzer, statuses []v1.ContainerSt
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
} else if containerStatus.State.Terminated != nil {
|
||||
if containerStatus.State.Terminated.ExitCode != 0 {
|
||||
// This represents a container that is terminated abnormally
|
||||
// https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-state-terminated
|
||||
exitCode := containerStatus.State.Terminated.ExitCode
|
||||
reason := containerStatus.State.Terminated.Reason
|
||||
if reason == "" {
|
||||
reason = "Unknown"
|
||||
}
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("the termination reason is %s exitCode=%d container=%s pod=%s", reason, exitCode, containerStatus.Name, name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// when pod is Running but its ReadinessProbe fails
|
||||
if !containerStatus.Ready && statusPhase == "Running" {
|
||||
|
||||
@@ -343,6 +343,57 @@ func TestPodAnalyzer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Terminated container with non-zero exit code",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Pod1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodFailed,
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
Name: "Container1",
|
||||
Ready: false,
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{
|
||||
ExitCode: 1,
|
||||
Reason: "Error",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Container2",
|
||||
Ready: false,
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{
|
||||
ExitCode: 2,
|
||||
Reason: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/Pod1",
|
||||
failuresCount: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
podAnalyzer := PodAnalyzer{}
|
||||
|
||||
201
pkg/analyzer/security.go
Normal file
201
pkg/analyzer/security.go
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
Copyright 2024 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type SecurityAnalyzer struct{}
|
||||
|
||||
func (SecurityAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
kind := "Security"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
var results []common.Result
|
||||
|
||||
// Analyze ServiceAccounts
|
||||
saResults, err := analyzeServiceAccounts(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, saResults...)
|
||||
|
||||
// Analyze RoleBindings
|
||||
rbResults, err := analyzeRoleBindings(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, rbResults...)
|
||||
|
||||
// Analyze Pod Security Contexts
|
||||
podResults, err := analyzePodSecurityContexts(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, podResults...)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func analyzeServiceAccounts(a common.Analyzer) ([]common.Result, error) {
|
||||
var results []common.Result
|
||||
|
||||
sas, err := a.Client.GetClient().CoreV1().ServiceAccounts(a.Namespace).List(a.Context, metav1.ListOptions{
|
||||
LabelSelector: a.LabelSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sa := range sas.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for default service account usage
|
||||
if sa.Name == "default" {
|
||||
pods, err := a.Client.GetClient().CoreV1().Pods(sa.Namespace).List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
defaultSAUsers := []string{}
|
||||
for _, pod := range pods.Items {
|
||||
if pod.Spec.ServiceAccountName == "default" {
|
||||
defaultSAUsers = append(defaultSAUsers, pod.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(defaultSAUsers) > 0 {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Default service account is being used by pods: %v", defaultSAUsers),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: "Security/ServiceAccount",
|
||||
Name: fmt.Sprintf("%s/%s", sa.Namespace, sa.Name),
|
||||
Error: failures,
|
||||
})
|
||||
AnalyzerErrorsMetric.WithLabelValues("Security/ServiceAccount", sa.Name, sa.Namespace).Set(float64(len(failures)))
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func analyzeRoleBindings(a common.Analyzer) ([]common.Result, error) {
|
||||
var results []common.Result
|
||||
|
||||
rbs, err := a.Client.GetClient().RbacV1().RoleBindings(a.Namespace).List(a.Context, metav1.ListOptions{
|
||||
LabelSelector: a.LabelSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rb := range rbs.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for wildcards in role references
|
||||
role, err := a.Client.GetClient().RbacV1().Roles(rb.Namespace).Get(a.Context, rb.RoleRef.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, rule := range role.Rules {
|
||||
if containsWildcard(rule.Verbs) || containsWildcard(rule.Resources) {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("RoleBinding %s references Role %s which contains wildcard permissions - this is not recommended for security best practices", rb.Name, role.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: "Security/RoleBinding",
|
||||
Name: fmt.Sprintf("%s/%s", rb.Namespace, rb.Name),
|
||||
Error: failures,
|
||||
})
|
||||
AnalyzerErrorsMetric.WithLabelValues("Security/RoleBinding", rb.Name, rb.Namespace).Set(float64(len(failures)))
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func analyzePodSecurityContexts(a common.Analyzer) ([]common.Result, error) {
|
||||
var results []common.Result
|
||||
|
||||
pods, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{
|
||||
LabelSelector: a.LabelSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pod := range pods.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for privileged containers first (most critical)
|
||||
hasPrivilegedContainer := false
|
||||
for _, container := range pod.Spec.Containers {
|
||||
if container.SecurityContext != nil && container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Container %s in pod %s is running as privileged which poses security risks", container.Name, pod.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
hasPrivilegedContainer = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Only check for missing security context if no privileged containers found
|
||||
if !hasPrivilegedContainer && pod.Spec.SecurityContext == nil {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Pod %s does not have a security context defined which may pose security risks", pod.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: "Security/Pod",
|
||||
Name: fmt.Sprintf("%s/%s", pod.Namespace, pod.Name),
|
||||
Error: failures[:1],
|
||||
})
|
||||
AnalyzerErrorsMetric.WithLabelValues("Security/Pod", pod.Name, pod.Namespace).Set(1)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func containsWildcard(slice []string) bool {
|
||||
for _, item := range slice {
|
||||
if item == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
181
pkg/analyzer/security_test.go
Normal file
181
pkg/analyzer/security_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
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/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestSecurityAnalyzer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
serviceAccounts []v1.ServiceAccount
|
||||
pods []v1.Pod
|
||||
roles []rbacv1.Role
|
||||
roleBindings []rbacv1.RoleBinding
|
||||
expectedErrors int
|
||||
expectedKinds []string
|
||||
}{
|
||||
{
|
||||
name: "default service account usage",
|
||||
namespace: "default",
|
||||
serviceAccounts: []v1.ServiceAccount{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
pods: []v1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pod",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
ServiceAccountName: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 2,
|
||||
expectedKinds: []string{"Security/ServiceAccount", "Security/Pod"},
|
||||
},
|
||||
{
|
||||
name: "privileged container",
|
||||
namespace: "default",
|
||||
pods: []v1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "privileged-pod",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "privileged-container",
|
||||
SecurityContext: &v1.SecurityContext{
|
||||
Privileged: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
expectedKinds: []string{"Security/Pod"},
|
||||
},
|
||||
{
|
||||
name: "wildcard permissions in role",
|
||||
namespace: "default",
|
||||
roles: []rbacv1.Role{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "wildcard-role",
|
||||
Namespace: "default",
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
Verbs: []string{"*"},
|
||||
Resources: []string{"pods"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
roleBindings: []rbacv1.RoleBinding{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-binding",
|
||||
Namespace: "default",
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
Kind: "Role",
|
||||
Name: "wildcard-role",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
expectedKinds: []string{"Security/RoleBinding"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
// Create test resources
|
||||
for _, sa := range tt.serviceAccounts {
|
||||
_, err := client.CoreV1().ServiceAccounts(tt.namespace).Create(context.TODO(), &sa, metav1.CreateOptions{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
for _, pod := range tt.pods {
|
||||
_, err := client.CoreV1().Pods(tt.namespace).Create(context.TODO(), &pod, metav1.CreateOptions{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
for _, role := range tt.roles {
|
||||
_, err := client.RbacV1().Roles(tt.namespace).Create(context.TODO(), &role, metav1.CreateOptions{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
for _, rb := range tt.roleBindings {
|
||||
_, err := client.RbacV1().RoleBindings(tt.namespace).Create(context.TODO(), &rb, metav1.CreateOptions{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
analyzer := SecurityAnalyzer{}
|
||||
results, err := analyzer.Analyze(common.Analyzer{
|
||||
Client: &kubernetes.Client{Client: client},
|
||||
Context: context.TODO(),
|
||||
Namespace: tt.namespace,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Debug: Print all results
|
||||
t.Logf("Got %d results:", len(results))
|
||||
for _, result := range results {
|
||||
t.Logf(" Kind: %s, Name: %s", result.Kind, result.Name)
|
||||
for _, failure := range result.Error {
|
||||
t.Logf(" Failure: %s", failure.Text)
|
||||
}
|
||||
}
|
||||
|
||||
// Count results by kind
|
||||
resultsByKind := make(map[string]int)
|
||||
for _, result := range results {
|
||||
resultsByKind[result.Kind]++
|
||||
}
|
||||
|
||||
// Check that we have the expected number of results for each kind
|
||||
for _, expectedKind := range tt.expectedKinds {
|
||||
assert.Equal(t, 1, resultsByKind[expectedKind], "Expected 1 result of kind %s", expectedKind)
|
||||
}
|
||||
|
||||
// Check total number of results matches expected kinds
|
||||
assert.Equal(t, len(tt.expectedKinds), len(results), "Expected %d total results", len(tt.expectedKinds))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -24,145 +24,232 @@ import (
|
||||
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) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
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: "test",
|
||||
}
|
||||
|
||||
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
|
||||
tests := []struct {
|
||||
name string
|
||||
config common.Analyzer
|
||||
expectations []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "test/Endpoint1",
|
||||
failuresCount: 1,
|
||||
name: "Service with no endpoints",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{}, // Empty subsets
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/test-service",
|
||||
failuresCount: 1, // One failure for no endpoints
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test/Service1",
|
||||
failuresCount: 2,
|
||||
name: "Service with not ready endpoints",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{
|
||||
{
|
||||
NotReadyAddresses: []v1.EndpointAddress{
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "Pod",
|
||||
Name: "test-pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/test-service",
|
||||
failuresCount: 1, // One failure for not ready endpoints
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Service with warning events",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{}, // Empty subsets
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-event",
|
||||
Namespace: "default",
|
||||
},
|
||||
InvolvedObject: v1.ObjectReference{
|
||||
Kind: "Service",
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Type: "Warning",
|
||||
Reason: "TestReason",
|
||||
Message: "Test warning message",
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/test-service",
|
||||
failuresCount: 2, // One failure for no endpoints, one for warning event
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Service with leader election annotation",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"control-plane.alpha.kubernetes.io/leader": "test-leader",
|
||||
},
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{}, // Empty subsets
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
// No expectations for leader election endpoints
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Service with non-existent service",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{}, // Empty subsets
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
// No expectations for non-existent service
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
analyzer := ServiceAnalyzer{}
|
||||
results, err := analyzer.Analyze(tt.config)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, len(tt.expectations))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
// Sort results by name for consistent comparison
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
for i, expectation := range tt.expectations {
|
||||
require.Equal(t, expectation.name, results[i].Name)
|
||||
require.Len(t, results[i].Error, expectation.failuresCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -384,7 +384,7 @@ func TestStatefulSetAnalyzerUnavailableReplicaWithPodInitialized(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
|
||||
var errorFound bool
|
||||
want := "Statefulset pod example-1 in the namespace default is not in running state."
|
||||
|
||||
|
||||
216
pkg/analyzer/storage.go
Normal file
216
pkg/analyzer/storage.go
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type StorageAnalyzer struct{}
|
||||
|
||||
func (StorageAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
kind := "Storage"
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
})
|
||||
|
||||
var results []common.Result
|
||||
|
||||
// Analyze StorageClasses
|
||||
scResults, err := analyzeStorageClasses(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, scResults...)
|
||||
|
||||
// Analyze PersistentVolumes
|
||||
pvResults, err := analyzePersistentVolumes(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, pvResults...)
|
||||
|
||||
// Analyze PVCs with enhanced checks
|
||||
pvcResults, err := analyzePersistentVolumeClaims(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, pvcResults...)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func analyzeStorageClasses(a common.Analyzer) ([]common.Result, error) {
|
||||
var results []common.Result
|
||||
|
||||
scs, err := a.Client.GetClient().StorageV1().StorageClasses().List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sc := range scs.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for deprecated storage classes
|
||||
if sc.Provisioner == "kubernetes.io/no-provisioner" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("StorageClass %s uses deprecated provisioner 'kubernetes.io/no-provisioner'", sc.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
|
||||
// Check for default storage class
|
||||
if sc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" {
|
||||
// Check if there are multiple default storage classes
|
||||
defaultCount := 0
|
||||
for _, otherSc := range scs.Items {
|
||||
if otherSc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" {
|
||||
defaultCount++
|
||||
}
|
||||
}
|
||||
if defaultCount > 1 {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Multiple default StorageClasses found (%d), which can cause confusion", defaultCount),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: "Storage/StorageClass",
|
||||
Name: sc.Name,
|
||||
Error: failures,
|
||||
})
|
||||
AnalyzerErrorsMetric.WithLabelValues("Storage/StorageClass", sc.Name, "").Set(float64(len(failures)))
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func analyzePersistentVolumes(a common.Analyzer) ([]common.Result, error) {
|
||||
var results []common.Result
|
||||
|
||||
pvs, err := a.Client.GetClient().CoreV1().PersistentVolumes().List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pv := range pvs.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for released PVs
|
||||
if pv.Status.Phase == v1.VolumeReleased {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("PersistentVolume %s is in Released state and should be cleaned up", pv.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
|
||||
// Check for failed PVs
|
||||
if pv.Status.Phase == v1.VolumeFailed {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("PersistentVolume %s is in Failed state", pv.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
|
||||
// Check for small PVs (less than 1Gi)
|
||||
if capacity, ok := pv.Spec.Capacity[v1.ResourceStorage]; ok {
|
||||
if capacity.Cmp(resource.MustParse("1Gi")) < 0 {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("PersistentVolume %s has small capacity (%s)", pv.Name, capacity.String()),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: "Storage/PersistentVolume",
|
||||
Name: pv.Name,
|
||||
Error: failures,
|
||||
})
|
||||
AnalyzerErrorsMetric.WithLabelValues("Storage/PersistentVolume", pv.Name, "").Set(float64(len(failures)))
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func analyzePersistentVolumeClaims(a common.Analyzer) ([]common.Result, error) {
|
||||
var results []common.Result
|
||||
|
||||
pvcs, err := a.Client.GetClient().CoreV1().PersistentVolumeClaims(a.Namespace).List(a.Context, metav1.ListOptions{
|
||||
LabelSelector: a.LabelSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pvc := range pvcs.Items {
|
||||
var failures []common.Failure
|
||||
|
||||
// Check for PVC state issues first (most critical)
|
||||
switch pvc.Status.Phase {
|
||||
case v1.ClaimPending:
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("PersistentVolumeClaim %s is in Pending state", pvc.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
case v1.ClaimLost:
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("PersistentVolumeClaim %s is in Lost state", pvc.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
default:
|
||||
// Only check other issues if PVC is not in a critical state
|
||||
if capacity, ok := pvc.Spec.Resources.Requests[v1.ResourceStorage]; ok {
|
||||
if capacity.Cmp(resource.MustParse("1Gi")) < 0 {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("PersistentVolumeClaim %s has small capacity (%s)", pvc.Name, capacity.String()),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing storage class
|
||||
if pvc.Spec.StorageClassName == nil && pvc.Spec.VolumeName == "" {
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("PersistentVolumeClaim %s has no StorageClass specified", pvc.Name),
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Only report the first failure found
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: "Storage/PersistentVolumeClaim",
|
||||
Name: fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name),
|
||||
Error: failures[:1],
|
||||
})
|
||||
AnalyzerErrorsMetric.WithLabelValues("Storage/PersistentVolumeClaim", pvc.Name, pvc.Namespace).Set(1)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
254
pkg/analyzer/storage_test.go
Normal file
254
pkg/analyzer/storage_test.go
Normal file
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
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"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestStorageAnalyzer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespace string
|
||||
storageClasses []storagev1.StorageClass
|
||||
pvs []v1.PersistentVolume
|
||||
pvcs []v1.PersistentVolumeClaim
|
||||
expectedErrors int
|
||||
}{
|
||||
{
|
||||
name: "Deprecated StorageClass",
|
||||
namespace: "default",
|
||||
storageClasses: []storagev1.StorageClass{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "deprecated-sc",
|
||||
},
|
||||
Provisioner: "kubernetes.io/no-provisioner",
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "Multiple Default StorageClasses",
|
||||
namespace: "default",
|
||||
storageClasses: []storagev1.StorageClass{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default-sc1",
|
||||
Annotations: map[string]string{
|
||||
"storageclass.kubernetes.io/is-default-class": "true",
|
||||
},
|
||||
},
|
||||
Provisioner: "kubernetes.io/gce-pd",
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default-sc2",
|
||||
Annotations: map[string]string{
|
||||
"storageclass.kubernetes.io/is-default-class": "true",
|
||||
},
|
||||
},
|
||||
Provisioner: "kubernetes.io/aws-ebs",
|
||||
},
|
||||
},
|
||||
expectedErrors: 2,
|
||||
},
|
||||
{
|
||||
name: "Released PV",
|
||||
namespace: "default",
|
||||
pvs: []v1.PersistentVolume{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "released-pv",
|
||||
},
|
||||
Status: v1.PersistentVolumeStatus{
|
||||
Phase: v1.VolumeReleased,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "Failed PV",
|
||||
namespace: "default",
|
||||
pvs: []v1.PersistentVolume{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "failed-pv",
|
||||
},
|
||||
Status: v1.PersistentVolumeStatus{
|
||||
Phase: v1.VolumeFailed,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "Small PV",
|
||||
namespace: "default",
|
||||
pvs: []v1.PersistentVolume{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "small-pv",
|
||||
},
|
||||
Spec: v1.PersistentVolumeSpec{
|
||||
Capacity: v1.ResourceList{
|
||||
v1.ResourceStorage: resource.MustParse("500Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "Pending PVC",
|
||||
namespace: "default",
|
||||
pvcs: []v1.PersistentVolumeClaim{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pending-pvc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PersistentVolumeClaimStatus{
|
||||
Phase: v1.ClaimPending,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "Lost PVC",
|
||||
namespace: "default",
|
||||
pvcs: []v1.PersistentVolumeClaim{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "lost-pvc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PersistentVolumeClaimStatus{
|
||||
Phase: v1.ClaimLost,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "Small PVC",
|
||||
namespace: "default",
|
||||
pvcs: []v1.PersistentVolumeClaim{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "small-pvc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.PersistentVolumeClaimSpec{
|
||||
Resources: v1.VolumeResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceStorage: resource.MustParse("500Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
{
|
||||
name: "PVC without StorageClass",
|
||||
namespace: "default",
|
||||
pvcs: []v1.PersistentVolumeClaim{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "no-sc-pvc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.PersistentVolumeClaimSpec{
|
||||
Resources: v1.VolumeResourceRequirements{
|
||||
Requests: v1.ResourceList{
|
||||
v1.ResourceStorage: resource.MustParse("1Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrors: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create fake client
|
||||
client := fake.NewSimpleClientset()
|
||||
|
||||
// Create test resources
|
||||
for _, sc := range tt.storageClasses {
|
||||
_, err := client.StorageV1().StorageClasses().Create(context.TODO(), &sc, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create StorageClass: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, pv := range tt.pvs {
|
||||
_, err := client.CoreV1().PersistentVolumes().Create(context.TODO(), &pv, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PV: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, pvc := range tt.pvcs {
|
||||
_, err := client.CoreV1().PersistentVolumeClaims(tt.namespace).Create(context.TODO(), &pvc, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create PVC: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create analyzer
|
||||
analyzer := StorageAnalyzer{}
|
||||
|
||||
// Create analyzer config
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: client,
|
||||
},
|
||||
Context: context.TODO(),
|
||||
Namespace: tt.namespace,
|
||||
}
|
||||
|
||||
// Run analysis
|
||||
results, err := analyzer.Analyze(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to run analysis: %v", err)
|
||||
}
|
||||
|
||||
// Count total errors
|
||||
totalErrors := 0
|
||||
for _, result := range results {
|
||||
totalErrors += len(result.Error)
|
||||
}
|
||||
|
||||
// Check error count
|
||||
if totalErrors != tt.expectedErrors {
|
||||
t.Errorf("Expected %d errors, got %d", tt.expectedErrors, totalErrors)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
pkg/analyzer/subscription.go
Normal file
55
pkg/analyzer/subscription.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type SubscriptionAnalyzer struct{}
|
||||
|
||||
var subGVR = schema.GroupVersionResource{
|
||||
Group: "operators.coreos.com", Version: "v1alpha1", Resource: "subscriptions",
|
||||
}
|
||||
|
||||
func (SubscriptionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
kind := "Subscription"
|
||||
if a.Client.GetDynamicClient() == nil {
|
||||
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
|
||||
}
|
||||
|
||||
list, err := a.Client.GetDynamicClient().
|
||||
Resource(subGVR).Namespace(metav1.NamespaceAll).
|
||||
List(a.Context, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []common.Result
|
||||
for _, item := range list.Items {
|
||||
ns, name := item.GetNamespace(), item.GetName()
|
||||
state, _, _ := unstructured.NestedString(item.Object, "status", "state")
|
||||
conds, _, _ := unstructured.NestedSlice(item.Object, "status", "conditions")
|
||||
|
||||
var failures []common.Failure
|
||||
if state == "" || state == "UpgradePending" || state == "UpgradeAvailable" {
|
||||
msg := "subscription not at latest"
|
||||
if c := pickWorstCondition(conds); c != "" {
|
||||
msg += "; " + c
|
||||
}
|
||||
failures = append(failures, common.Failure{Text: fmt.Sprintf("state=%q: %s", state, msg)})
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
results = append(results, common.Result{
|
||||
Kind: kind,
|
||||
Name: ns + "/" + name,
|
||||
Error: failures,
|
||||
})
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
78
pkg/analyzer/subscription_test.go
Normal file
78
pkg/analyzer/subscription_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
)
|
||||
|
||||
func TestSubscriptionAnalyzer(t *testing.T) {
|
||||
ok := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1alpha1",
|
||||
"kind": "Subscription",
|
||||
"metadata": map[string]any{
|
||||
"name": "ok-sub",
|
||||
"namespace": "ns1",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"state": "AtLatestKnown",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bad := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "operators.coreos.com/v1alpha1",
|
||||
"kind": "Subscription",
|
||||
"metadata": map[string]any{
|
||||
"name": "upgrade-sub",
|
||||
"namespace": "ns1",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"state": "UpgradeAvailable",
|
||||
"conditions": []interface{}{
|
||||
map[string]any{
|
||||
"status": "False",
|
||||
"reason": "CatalogSourcesUnhealthy",
|
||||
"message": "not reachable",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
listKinds := map[schema.GroupVersionResource]string{
|
||||
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "subscriptions"}: "SubscriptionList",
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ok, bad)
|
||||
|
||||
a := common.Analyzer{
|
||||
Context: context.TODO(),
|
||||
Client: &kubernetes.Client{DynamicClient: dc},
|
||||
}
|
||||
|
||||
res, err := (SubscriptionAnalyzer{}).Analyze(a)
|
||||
if err != nil {
|
||||
t.Fatalf("Analyze error: %v", err)
|
||||
}
|
||||
|
||||
if len(res) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(res))
|
||||
}
|
||||
if res[0].Kind != "Subscription" || !strings.Contains(res[0].Name, "ns1/upgrade-sub") {
|
||||
t.Fatalf("unexpected result: %#v", res[0])
|
||||
}
|
||||
if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "CatalogSourcesUnhealthy") {
|
||||
t.Fatalf("expected 'CatalogSourcesUnhealthy' in failure, got %#v", res[0].Error)
|
||||
}
|
||||
}
|
||||
10
pkg/analyzer/test_utils.go
Normal file
10
pkg/analyzer/test_utils.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package analyzer
|
||||
|
||||
// Helper functions for tests
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func int64Ptr(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
18
pkg/cache/cache.go
vendored
18
pkg/cache/cache.go
vendored
@@ -14,6 +14,7 @@ var (
|
||||
&FileBasedCache{},
|
||||
&GCSCache{},
|
||||
&S3Cache{},
|
||||
&InterplexCache{},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -54,15 +55,21 @@ func NewCacheProvider(cacheType, bucketname, region, endpoint, storageAccount, c
|
||||
case cacheType == "azure":
|
||||
cProvider.Azure.ContainerName = containerName
|
||||
cProvider.Azure.StorageAccount = storageAccount
|
||||
cProvider.CurrentCacheType = "azure"
|
||||
case cacheType == "gcs":
|
||||
cProvider.GCS.BucketName = bucketname
|
||||
cProvider.GCS.ProjectId = projectId
|
||||
cProvider.GCS.Region = region
|
||||
cProvider.CurrentCacheType = "gcs"
|
||||
case cacheType == "s3":
|
||||
cProvider.S3.BucketName = bucketname
|
||||
cProvider.S3.Region = region
|
||||
cProvider.S3.Endpoint = endpoint
|
||||
cProvider.S3.InsecureSkipVerify = insecure
|
||||
cProvider.CurrentCacheType = "s3"
|
||||
case cacheType == "interplex":
|
||||
cProvider.Interplex.ConnectionString = endpoint
|
||||
cProvider.CurrentCacheType = "interplex"
|
||||
default:
|
||||
return CacheProvider{}, status.Error(codes.Internal, fmt.Sprintf("%s is not a valid option", cacheType))
|
||||
}
|
||||
@@ -83,20 +90,19 @@ func GetCacheConfiguration() (ICache, error) {
|
||||
}
|
||||
|
||||
var cache ICache
|
||||
|
||||
switch {
|
||||
case cacheInfo.GCS != GCSCacheConfiguration{}:
|
||||
case cacheInfo.CurrentCacheType == "gcs":
|
||||
cache = &GCSCache{}
|
||||
case cacheInfo.Azure != AzureCacheConfiguration{}:
|
||||
case cacheInfo.CurrentCacheType == "azure":
|
||||
cache = &AzureCache{}
|
||||
case cacheInfo.S3 != S3CacheConfiguration{}:
|
||||
case cacheInfo.CurrentCacheType == "s3":
|
||||
cache = &S3Cache{}
|
||||
case cacheInfo.CurrentCacheType == "interplex":
|
||||
cache = &InterplexCache{}
|
||||
default:
|
||||
cache = &FileBasedCache{}
|
||||
}
|
||||
|
||||
err_config := cache.Configure(cacheInfo)
|
||||
|
||||
return cache, err_config
|
||||
}
|
||||
|
||||
|
||||
60
pkg/cache/cache_test.go
vendored
Normal file
60
pkg/cache/cache_test.go
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewReturnsExpectedCache(t *testing.T) {
|
||||
require.IsType(t, &FileBasedCache{}, New("file"))
|
||||
require.IsType(t, &AzureCache{}, New("azure"))
|
||||
require.IsType(t, &GCSCache{}, New("gcs"))
|
||||
require.IsType(t, &S3Cache{}, New("s3"))
|
||||
require.IsType(t, &InterplexCache{}, New("interplex"))
|
||||
// default fallback
|
||||
require.IsType(t, &FileBasedCache{}, New("unknown"))
|
||||
}
|
||||
|
||||
func TestNewCacheProvider_InterplexAndInvalid(t *testing.T) {
|
||||
// valid: interplex
|
||||
cp, err := NewCacheProvider("interplex", "", "", "localhost:1", "", "", "", false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "interplex", cp.CurrentCacheType)
|
||||
require.Equal(t, "localhost:1", cp.Interplex.ConnectionString)
|
||||
|
||||
// invalid type
|
||||
_, err = NewCacheProvider("not-a-type", "", "", "", "", "", "", false)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAddRemoveRemoteCacheAndGet(t *testing.T) {
|
||||
// isolate viper with temp config file
|
||||
tmpFile, err := os.CreateTemp("", "k8sgpt-cache-config-*.yaml")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = os.Remove(tmpFile.Name())
|
||||
}()
|
||||
viper.Reset()
|
||||
viper.SetConfigFile(tmpFile.Name())
|
||||
|
||||
// add interplex remote cache
|
||||
cp := CacheProvider{}
|
||||
cp.CurrentCacheType = "interplex"
|
||||
cp.Interplex.ConnectionString = "localhost:1"
|
||||
require.NoError(t, AddRemoteCache(cp))
|
||||
|
||||
// read back via GetCacheConfiguration
|
||||
c, err := GetCacheConfiguration()
|
||||
require.NoError(t, err)
|
||||
require.IsType(t, &InterplexCache{}, c)
|
||||
|
||||
// remove remote cache
|
||||
require.NoError(t, RemoveRemoteCache())
|
||||
// now default should be file-based
|
||||
c2, err := GetCacheConfiguration()
|
||||
require.NoError(t, err)
|
||||
require.IsType(t, &FileBasedCache{}, c2)
|
||||
}
|
||||
77
pkg/cache/file_based_test.go
vendored
Normal file
77
pkg/cache/file_based_test.go
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// withTempCacheHome sets XDG_CACHE_HOME to a temp dir for test isolation.
|
||||
func withTempCacheHome(t *testing.T) func() {
|
||||
t.Helper()
|
||||
tmp, err := os.MkdirTemp("", "k8sgpt-cache-test-*")
|
||||
require.NoError(t, err)
|
||||
old := os.Getenv("XDG_CACHE_HOME")
|
||||
require.NoError(t, os.Setenv("XDG_CACHE_HOME", tmp))
|
||||
return func() {
|
||||
_ = os.Setenv("XDG_CACHE_HOME", old)
|
||||
_ = os.RemoveAll(tmp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileBasedCache_BasicOps(t *testing.T) {
|
||||
cleanup := withTempCacheHome(t)
|
||||
defer cleanup()
|
||||
|
||||
c := &FileBasedCache{}
|
||||
// Configure should be a no-op
|
||||
require.NoError(t, c.Configure(CacheProvider{}))
|
||||
require.Equal(t, "file", c.GetName())
|
||||
require.False(t, c.IsCacheDisabled())
|
||||
c.DisableCache()
|
||||
require.True(t, c.IsCacheDisabled())
|
||||
|
||||
key := "testkey"
|
||||
data := "hello"
|
||||
|
||||
// Store
|
||||
require.NoError(t, c.Store(key, data))
|
||||
|
||||
// Exists
|
||||
require.True(t, c.Exists(key))
|
||||
|
||||
// Load
|
||||
got, err := c.Load(key)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, got)
|
||||
|
||||
// List should include our key file
|
||||
items, err := c.List()
|
||||
require.NoError(t, err)
|
||||
// ensure at least one item and that one matches our key
|
||||
found := false
|
||||
for _, it := range items {
|
||||
if it.Name == key {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found)
|
||||
|
||||
// Remove
|
||||
require.NoError(t, c.Remove(key))
|
||||
require.False(t, c.Exists(key))
|
||||
}
|
||||
|
||||
func TestFileBasedCache_PathShape(t *testing.T) {
|
||||
cleanup := withTempCacheHome(t)
|
||||
defer cleanup()
|
||||
// Verify xdg.CacheFile path shape (directory and filename)
|
||||
p, err := xdg.CacheFile(filepath.Join("k8sgpt", "abc"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "abc", filepath.Base(p))
|
||||
require.Contains(t, p, "k8sgpt")
|
||||
}
|
||||
131
pkg/cache/interplex_based.go
vendored
Normal file
131
pkg/cache/interplex_based.go
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
rpc "buf.build/gen/go/interplex-ai/schemas/grpc/go/protobuf/schema/v1/schemav1grpc"
|
||||
schemav1 "buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go/protobuf/schema/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
var _ ICache = (*InterplexCache)(nil)
|
||||
|
||||
type InterplexCache struct {
|
||||
configuration InterplexCacheConfiguration
|
||||
client InterplexClient
|
||||
cacheServiceClient rpc.CacheServiceClient
|
||||
noCache bool
|
||||
}
|
||||
|
||||
type InterplexCacheConfiguration struct {
|
||||
ConnectionString string `mapstructure:"connectionString" yaml:"connectionString,omitempty"`
|
||||
}
|
||||
|
||||
type InterplexClient struct {
|
||||
}
|
||||
|
||||
func (c *InterplexCache) Configure(cacheInfo CacheProvider) error {
|
||||
|
||||
if cacheInfo.Interplex.ConnectionString == "" {
|
||||
return errors.New("connection string is required")
|
||||
}
|
||||
c.configuration.ConnectionString = cacheInfo.Interplex.ConnectionString
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *InterplexCache) Store(key string, data string) error {
|
||||
|
||||
if os.Getenv("INTERPLEX_LOCAL_MODE") != "" {
|
||||
c.configuration.ConnectionString = "localhost:8084"
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(c.configuration.ConnectionString, grpc.WithInsecure(), grpc.WithBlock())
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceClient := rpc.NewCacheServiceClient(conn)
|
||||
c.cacheServiceClient = serviceClient
|
||||
req := schemav1.SetRequest{
|
||||
Key: key,
|
||||
Value: data,
|
||||
}
|
||||
_, err = c.cacheServiceClient.Set(context.Background(), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *InterplexCache) Load(key string) (string, error) {
|
||||
if os.Getenv("INTERPLEX_LOCAL_MODE") != "" {
|
||||
c.configuration.ConnectionString = "localhost:8084"
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(c.configuration.ConnectionString, grpc.WithInsecure(), grpc.WithBlock())
|
||||
defer conn.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
serviceClient := rpc.NewCacheServiceClient(conn)
|
||||
c.cacheServiceClient = serviceClient
|
||||
req := schemav1.GetRequest{
|
||||
Key: key,
|
||||
}
|
||||
resp, err := c.cacheServiceClient.Get(context.Background(), &req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Value, nil
|
||||
}
|
||||
|
||||
func (c *InterplexCache) List() ([]CacheObjectDetails, error) {
|
||||
// Not implemented for Interplex cache
|
||||
return []CacheObjectDetails{}, nil
|
||||
}
|
||||
|
||||
func (c *InterplexCache) Remove(key string) error {
|
||||
if os.Getenv("INTERPLEX_LOCAL_MODE") != "" {
|
||||
c.configuration.ConnectionString = "localhost:8084"
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(c.configuration.ConnectionString, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := conn.Close(); err != nil {
|
||||
// Log the error but don't return it since this is a deferred function
|
||||
fmt.Printf("Error closing connection: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
serviceClient := rpc.NewCacheServiceClient(conn)
|
||||
c.cacheServiceClient = serviceClient
|
||||
req := schemav1.DeleteRequest{
|
||||
Key: key,
|
||||
}
|
||||
_, err = c.cacheServiceClient.Delete(context.Background(), &req)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *InterplexCache) Exists(key string) bool {
|
||||
_, err := c.Load(key)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (c *InterplexCache) IsCacheDisabled() bool {
|
||||
return c.noCache
|
||||
}
|
||||
|
||||
func (c *InterplexCache) GetName() string {
|
||||
return "interplex"
|
||||
}
|
||||
|
||||
func (c *InterplexCache) DisableCache() {
|
||||
c.noCache = true
|
||||
}
|
||||
90
pkg/cache/interplex_based_test.go
vendored
Normal file
90
pkg/cache/interplex_based_test.go
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
rpc "buf.build/gen/go/interplex-ai/schemas/grpc/go/protobuf/schema/v1/schemav1grpc"
|
||||
schemav1 "buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go/protobuf/schema/v1"
|
||||
"context"
|
||||
"errors"
|
||||
"google.golang.org/grpc"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInterplexCache(t *testing.T) {
|
||||
cache := &InterplexCache{
|
||||
configuration: InterplexCacheConfiguration{
|
||||
ConnectionString: "localhost:50051",
|
||||
},
|
||||
}
|
||||
|
||||
// Mock GRPC server setup
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
lis, err := net.Listen("tcp", ":50051")
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
s := grpc.NewServer()
|
||||
rpc.RegisterCacheServiceServer(s, &mockCacheService{})
|
||||
if err := s.Serve(lis); err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// Check if server startup failed
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
t.Fatalf("failed to start mock server: %v", err)
|
||||
}
|
||||
default:
|
||||
// Server started successfully
|
||||
}
|
||||
|
||||
t.Run("TestStore", func(t *testing.T) {
|
||||
err := cache.Store("key1", "value1")
|
||||
if err != nil {
|
||||
t.Errorf("Error storing value: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TestLoad", func(t *testing.T) {
|
||||
value, err := cache.Load("key1")
|
||||
if err != nil {
|
||||
t.Errorf("Error loading value: %v", err)
|
||||
}
|
||||
if value != "value1" {
|
||||
t.Errorf("Expected value1, got %v", value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TestExists", func(t *testing.T) {
|
||||
exists := cache.Exists("key1")
|
||||
if !exists {
|
||||
t.Errorf("Expected key1 to exist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type mockCacheService struct {
|
||||
rpc.UnimplementedCacheServiceServer
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func (m *mockCacheService) Set(ctx context.Context, req *schemav1.SetRequest) (*schemav1.SetResponse, error) {
|
||||
if m.data == nil {
|
||||
m.data = make(map[string]string)
|
||||
}
|
||||
m.data[req.Key] = req.Value
|
||||
return &schemav1.SetResponse{}, nil
|
||||
}
|
||||
|
||||
func (m *mockCacheService) Get(ctx context.Context, req *schemav1.GetRequest) (*schemav1.GetResponse, error) {
|
||||
value, exists := m.data[req.Key]
|
||||
if !exists {
|
||||
return nil, errors.New("key not found")
|
||||
}
|
||||
return &schemav1.GetResponse{Value: value}, nil
|
||||
}
|
||||
18
pkg/cache/s3_based.go
vendored
18
pkg/cache/s3_based.go
vendored
@@ -3,8 +3,9 @@ package cache
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
@@ -27,16 +28,19 @@ type S3CacheConfiguration struct {
|
||||
|
||||
func (s *S3Cache) Configure(cacheInfo CacheProvider) error {
|
||||
if cacheInfo.S3.BucketName == "" {
|
||||
log.Fatal("Bucket name not configured")
|
||||
return errors.New("bucket name not configured")
|
||||
}
|
||||
s.bucketName = cacheInfo.S3.BucketName
|
||||
|
||||
sess := session.Must(session.NewSessionWithOptions(session.Options{
|
||||
sess, err := session.NewSessionWithOptions(session.Options{
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
Config: aws.Config{
|
||||
Region: aws.String(cacheInfo.S3.Region),
|
||||
},
|
||||
}))
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("failed to create AWS session; please check your AWS credentials and configuration: " + err.Error())
|
||||
}
|
||||
if cacheInfo.S3.Endpoint != "" {
|
||||
sess.Config.Endpoint = &cacheInfo.S3.Endpoint
|
||||
sess.Config.S3ForcePathStyle = aws.Bool(true)
|
||||
@@ -50,10 +54,14 @@ func (s *S3Cache) Configure(cacheInfo CacheProvider) error {
|
||||
s3Client := s3.New(sess)
|
||||
|
||||
// Check if the bucket exists, if not create it
|
||||
_, err := s3Client.HeadBucket(&s3.HeadBucketInput{
|
||||
_, err = s3Client.HeadBucket(&s3.HeadBucketInput{
|
||||
Bucket: aws.String(cacheInfo.S3.BucketName),
|
||||
})
|
||||
if err != nil {
|
||||
// Check for AWS credentials error
|
||||
if strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "SignatureDoesNotMatch") || strings.Contains(err.Error(), "NoCredentialProviders") {
|
||||
return errors.New("aws credentials are invalid or missing; please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or AWS config")
|
||||
}
|
||||
_, err = s3Client.CreateBucket(&s3.CreateBucketInput{
|
||||
Bucket: aws.String(cacheInfo.S3.BucketName),
|
||||
})
|
||||
|
||||
8
pkg/cache/types.go
vendored
8
pkg/cache/types.go
vendored
@@ -3,9 +3,11 @@ package cache
|
||||
import "time"
|
||||
|
||||
type CacheProvider struct {
|
||||
GCS GCSCacheConfiguration `mapstructucre:"gcs" yaml:"gcs,omitempty"`
|
||||
Azure AzureCacheConfiguration `mapstructucre:"azure" yaml:"azure,omitempty"`
|
||||
S3 S3CacheConfiguration `mapstructucre:"s3" yaml:"s3,omitempty"`
|
||||
CurrentCacheType string `mapstructure:"currentCacheType" yaml:"currentCacheType"`
|
||||
GCS GCSCacheConfiguration `mapstructure:"gcs" yaml:"gcs,omitempty"`
|
||||
Azure AzureCacheConfiguration `mapstructure:"azure" yaml:"azure,omitempty"`
|
||||
S3 S3CacheConfiguration `mapstructure:"s3" yaml:"s3,omitempty"`
|
||||
Interplex InterplexCacheConfiguration `mapstructure:"interplex" yaml:"interplex,omitempty"`
|
||||
}
|
||||
|
||||
type CacheObjectDetails struct {
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
trivy "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
|
||||
openapi_v2 "github.com/google/gnostic/openapiv2"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
@@ -29,6 +28,7 @@ import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
networkv1 "k8s.io/api/networking/v1"
|
||||
policyv1 "k8s.io/api/policy/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
@@ -67,10 +67,10 @@ type PreAnalysis struct {
|
||||
HTTPRoute gtwapi.HTTPRoute
|
||||
// Integrations
|
||||
ScaledObject keda.ScaledObject
|
||||
TrivyVulnerabilityReport trivy.VulnerabilityReport
|
||||
TrivyConfigAuditReport trivy.ConfigAuditReport
|
||||
KyvernoPolicyReport kyverno.PolicyReport
|
||||
KyvernoClusterPolicyReport kyverno.ClusterPolicyReport
|
||||
Catalog ClusterCatalog
|
||||
Extension ClusterExtension
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -96,3 +96,117 @@ type Sensitive struct {
|
||||
Unmasked string
|
||||
Masked string
|
||||
}
|
||||
|
||||
type (
|
||||
SourceType string
|
||||
AvailabilityMode string
|
||||
UpgradeConstraintPolicy string
|
||||
CRDUpgradeSafetyEnforcement string
|
||||
)
|
||||
|
||||
type ClusterCatalog struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata"`
|
||||
Spec ClusterCatalogSpec `json:"spec"`
|
||||
Status ClusterCatalogStatus `json:"status,omitempty"`
|
||||
}
|
||||
type ClusterCatalogList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata"`
|
||||
Items []ClusterCatalog `json:"items"`
|
||||
}
|
||||
|
||||
type ClusterCatalogSpec struct {
|
||||
Source CatalogSource `json:"source"`
|
||||
|
||||
Priority int32 `json:"priority"`
|
||||
|
||||
AvailabilityMode AvailabilityMode `json:"availabilityMode,omitempty"`
|
||||
}
|
||||
|
||||
type ClusterCatalogStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
|
||||
|
||||
ResolvedSource *ResolvedCatalogSource `json:"resolvedSource,omitempty"`
|
||||
|
||||
URLs *ClusterCatalogURLs `json:"urls,omitempty"`
|
||||
LastUnpacked *metav1.Time `json:"lastUnpacked,omitempty"`
|
||||
}
|
||||
|
||||
type ClusterCatalogURLs struct {
|
||||
Base string `json:"base"`
|
||||
}
|
||||
type CatalogSource struct {
|
||||
Type SourceType `json:"type"`
|
||||
Image *ImageSource `json:"image,omitempty"`
|
||||
}
|
||||
type ResolvedCatalogSource struct {
|
||||
Type SourceType `json:"type"`
|
||||
Image *ResolvedImageSource `json:"image"`
|
||||
}
|
||||
type ResolvedImageSource struct {
|
||||
Ref string `json:"ref"`
|
||||
}
|
||||
|
||||
type ImageSource struct {
|
||||
Ref string `json:"ref"`
|
||||
PollIntervalMinutes *int `json:"pollIntervalMinutes,omitempty"`
|
||||
}
|
||||
|
||||
type ClusterExtension struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
Spec ClusterExtensionSpec `json:"spec,omitempty"`
|
||||
Status ClusterExtensionStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type ClusterExtensionSpec struct {
|
||||
Namespace string `json:"namespace"`
|
||||
ServiceAccount ServiceAccountReference `json:"serviceAccount"`
|
||||
Source SourceConfig `json:"source"`
|
||||
Install *ClusterExtensionInstallConfig `json:"install,omitempty"`
|
||||
}
|
||||
|
||||
type ClusterExtensionInstallConfig struct {
|
||||
Preflight *PreflightConfig `json:"preflight,omitempty"`
|
||||
}
|
||||
|
||||
type PreflightConfig struct {
|
||||
CRDUpgradeSafety *CRDUpgradeSafetyPreflightConfig `json:"crdUpgradeSafety"`
|
||||
}
|
||||
|
||||
type CRDUpgradeSafetyPreflightConfig struct {
|
||||
Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"`
|
||||
}
|
||||
|
||||
type ServiceAccountReference struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SourceConfig struct {
|
||||
SourceType string `json:"sourceType"`
|
||||
Catalog *CatalogFilter `json:"catalog,omitempty"`
|
||||
}
|
||||
|
||||
type CatalogFilter struct {
|
||||
PackageName string `json:"packageName"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Channels []string `json:"channels,omitempty"`
|
||||
Selector *metav1.LabelSelector `json:"selector,omitempty"`
|
||||
UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"`
|
||||
}
|
||||
|
||||
type ClusterExtensionStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
|
||||
|
||||
Install *ClusterExtensionInstallStatus `json:"install,omitempty"`
|
||||
}
|
||||
|
||||
type ClusterExtensionInstallStatus struct {
|
||||
Bundle BundleMetadata `json:"bundle"`
|
||||
}
|
||||
|
||||
type BundleMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user