Compare commits

...

8 Commits

Author SHA1 Message Date
github-actions[bot]
00c99dc934 chore(main): release 0.4.23 (#1549)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-10 07:48:03 +01:00
Jian Zhang
a821814125 feat: add ClusterCatalog and ClusterExtension analyzers (#1555)
Signed-off-by: Jian Zhang <jiazha@redhat.com>
2025-08-08 17:12:05 +01:00
renovate[bot]
50d5d78c06 fix(deps): update module gopkg.in/yaml.v2 to v3 (#1537)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-20 20:27:15 +01:00
renovate[bot]
5b4224951e fix(deps): update module helm.sh/helm/v3 to v3.17.4 [security] (#1541)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-20 19:20:42 +01:00
Anders Swanson
290a4be210 feat: oci genai chat models (#1337)
Signed-off-by: Anders Swanson <anders.swanson@oracle.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-07-20 10:02:47 +01:00
github-actions[bot]
fe4608793d chore(main): release 0.4.22 (#1545)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-18 15:28:01 +01:00
Umesh Kaul
3a1187ad5a feat: add streamable-http support for MCP server (#1546)
* use mcp library to support streamable http and fix resourceResponse

Signed-off-by: Umesh Kaul <umeshkaul@gmail.com>

* added name and version for mcp server

Signed-off-by: Umesh Kaul <umeshkaul@gmail.com>

* added tests

Signed-off-by: Umesh Kaul <umeshkaul@gmail.com>

* chore: fixed linter

Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* fixed linter issues in server_test.go

Signed-off-by: Umesh Kaul <umeshkaul@gmail.com>

---------

Signed-off-by: Umesh Kaul <umeshkaul@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
Co-authored-by: AlexsJones <alexsimonjones@gmail.com>
2025-07-18 15:14:54 +01:00
koichi
1819e6f410 feat: add APAC region Claude models support for Amazon Bedrock (#1543)
Signed-off-by: Koichi Shimada <jumpe1programming@gmail.com>
2025-07-14 11:24:03 +01:00
18 changed files with 1239 additions and 124 deletions

View File

@@ -1 +1 @@
{".":"0.4.21"}
{".":"0.4.23"}

View File

@@ -1,5 +1,27 @@
# Changelog
## [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)

View File

@@ -62,7 +62,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.21/k8sgpt_386.rpm
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.23/k8sgpt_386.rpm
```
<!---x-release-please-end-->
@@ -70,7 +70,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.21/k8sgpt_amd64.rpm
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.23/k8sgpt_amd64.rpm
```
<!---x-release-please-end-->
</details>
@@ -83,7 +83,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.21/k8sgpt_386.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.23/k8sgpt_386.deb
sudo dpkg -i k8sgpt_386.deb
```
@@ -94,7 +94,7 @@ sudo dpkg -i k8sgpt_386.deb
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.21/k8sgpt_amd64.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.23/k8sgpt_amd64.deb
sudo dpkg -i k8sgpt_amd64.deb
```
@@ -109,7 +109,7 @@ sudo dpkg -i k8sgpt_amd64.deb
<!---x-release-please-start-version-->
```
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.21/k8sgpt_386.apk
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.23/k8sgpt_386.apk
apk add --allow-untrusted k8sgpt_386.apk
```
<!---x-release-please-end-->
@@ -118,7 +118,7 @@ sudo dpkg -i k8sgpt_amd64.deb
<!---x-release-please-start-version-->
```
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.21/k8sgpt_amd64.apk
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.23/k8sgpt_amd64.apk
apk add --allow-untrusted k8sgpt_amd64.apk
```
<!---x-release-please-end-->
@@ -270,6 +270,8 @@ you will be able to write your own analyzers.
- [x] logAnalyzer
- [x] storageAnalyzer
- [x] securityAnalyzer
- [x] ClusterCatalog
- [x] ClusterExtension
## Examples

View File

@@ -29,8 +29,10 @@ K8sGPT supports a variety of AI/LLM providers (backends). Some providers have a
- 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

18
go.mod
View File

@@ -14,7 +14,7 @@ require (
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.10.0
golang.org/x/term v0.30.0
helm.sh/helm/v3 v3.17.3
helm.sh/helm/v3 v3.17.4
k8s.io/api v0.32.2
k8s.io/apimachinery v0.32.2
k8s.io/client-go v0.32.2
@@ -97,7 +97,11 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/continuity v0.4.3 // indirect
@@ -113,6 +117,13 @@ require (
github.com/expr-lang/expr v1.17.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -126,7 +137,9 @@ require (
github.com/invopop/jsonschema v0.12.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/moby/sys/mountinfo v0.7.1 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
@@ -145,6 +158,8 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
@@ -156,6 +171,7 @@ require (
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
golang.org/x/arch v0.8.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect

39
go.sum
View File

@@ -789,6 +789,10 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
@@ -806,6 +810,10 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -918,7 +926,13 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
@@ -951,6 +965,14 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E=
github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
@@ -963,6 +985,8 @@ github.com/go-zookeeper/zk v1.0.4/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
@@ -1208,6 +1232,7 @@ github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuOb
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -1230,6 +1255,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
@@ -1473,6 +1500,10 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
@@ -1545,6 +1576,9 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -2226,8 +2260,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
helm.sh/helm/v3 v3.17.3 h1:3n5rW3D0ArjFl0p4/oWO8IbY/HKaNNwJtOQFdH2AZHg=
helm.sh/helm/v3 v3.17.3/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
helm.sh/helm/v3 v3.17.4 h1:GK+vgn9gKCyoH44+f3B5zpA78iH3AK4ywIInDEmmn/g=
helm.sh/helm/v3 v3.17.4/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -2294,6 +2328,7 @@ modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw
modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View File

@@ -95,7 +95,18 @@ var defaultModels = []bedrock_support.BedrockModel{
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{},
@@ -120,6 +131,18 @@ var defaultModels = []bedrock_support.BedrockModel{
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{},

View File

@@ -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)
}

View File

@@ -57,6 +57,8 @@ var additionalAnalyzerMap = map[string]common.IAnalyzer{
"HTTPRoute": HTTPRouteAnalyzer{},
"Storage": StorageAnalyzer{},
"Security": SecurityAnalyzer{},
"ClusterCatalog": ClusterCatalogAnalyzer{},
"ClusterExtension": ClusterExtensionAnalyzer{},
}
func ListFilters() ([]string, []string, []string) {

View 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)
}

View 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))
}

View 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
}

View 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))
}

View File

@@ -28,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"
)
@@ -68,6 +69,8 @@ type PreAnalysis struct {
ScaledObject keda.ScaledObject
KyvernoPolicyReport kyverno.PolicyReport
KyvernoClusterPolicyReport kyverno.ClusterPolicyReport
Catalog ClusterCatalog
Extension ClusterExtension
}
type Result struct {
@@ -93,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"`
}

View File

@@ -14,6 +14,7 @@ limitations under the License.
package kubernetes
import (
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
"k8s.io/client-go/rest"
@@ -33,6 +34,10 @@ func (c *Client) GetCtrlClient() ctrl.Client {
return c.CtrlClient
}
func (c *Client) GetDynamicClient() dynamic.Interface {
return c.DynamicClient
}
func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
var config *rest.Config
config, err := rest.InClusterConfig()
@@ -69,10 +74,16 @@ func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
return nil, err
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
return nil, err
}
return &Client{
Client: clientSet,
CtrlClient: ctrlClient,
Config: config,
ServerVersion: serverVersion,
DynamicClient: dynamicClient,
}, nil
}

View File

@@ -4,6 +4,7 @@ import (
openapi_v2 "github.com/google/gnostic/openapiv2"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime/pkg/client"
@@ -14,6 +15,7 @@ type Client struct {
CtrlClient ctrl.Client
Config *rest.Config
ServerVersion *version.Info
DynamicClient dynamic.Interface
}
type K8sApiReference struct {

View File

@@ -17,7 +17,6 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
@@ -25,6 +24,7 @@ import (
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/k8sgpt-ai/k8sgpt/pkg/server/config"
mcp_golang "github.com/metoro-io/mcp-golang"
mcp_http "github.com/metoro-io/mcp-golang/transport/http"
"github.com/metoro-io/mcp-golang/transport/stdio"
"github.com/spf13/viper"
"go.uber.org/zap"
@@ -41,10 +41,19 @@ type MCPServer struct {
// NewMCPServer creates a new MCP server
func NewMCPServer(port string, aiProvider *ai.AIProvider, useHTTP bool, logger *zap.Logger) (*MCPServer, error) {
// Create MCP server with stdio transport
transport := stdio.NewStdioServerTransport()
opts := []mcp_golang.ServerOptions{
mcp_golang.WithName("k8sgpt"),
mcp_golang.WithVersion("1.0.0"),
}
server := mcp_golang.NewServer(transport)
var server *mcp_golang.Server
if useHTTP {
logger.Info("starting MCP server with http transport on port", zap.String("port", port))
httpTransport := mcp_http.NewHTTPTransport("/mcp").WithAddr(":" + port)
server = mcp_golang.NewServer(httpTransport, opts...)
} else {
server = mcp_golang.NewServer(stdio.NewStdioServerTransport(), opts...)
}
return &MCPServer{
server: server,
@@ -86,20 +95,11 @@ func (s *MCPServer) Start() error {
return fmt.Errorf("failed to register prompts: %v", err)
}
if s.useHTTP {
// Start HTTP server
go func() {
http.HandleFunc("/mcp/analyze", s.handleAnalyzeHTTP)
http.HandleFunc("/mcp", s.handleSSE)
s.logger.Info("Starting MCP server on port", zap.String("port", s.port))
if err := http.ListenAndServe(fmt.Sprintf(":%s", s.port), nil); err != nil {
s.logger.Error("Error starting HTTP server", zap.Error(err))
}
}()
// Start the server (this will block)
if err := s.server.Serve(); err != nil {
s.logger.Error("Error starting MCP server", zap.Error(err))
}
// Start the server
return s.server.Serve()
return nil
}
// AnalyzeRequest represents the input parameters for the analyze tool
@@ -327,7 +327,7 @@ func (s *MCPServer) registerResources() error {
return nil
}
func (s *MCPServer) getClusterInfo(ctx context.Context) (interface{}, error) {
func (s *MCPServer) getClusterInfo(ctx context.Context) (*mcp_golang.ResourceResponse, error) {
// Create a new Kubernetes client
client, err := kubernetes.NewClient("", "")
if err != nil {
@@ -340,74 +340,27 @@ func (s *MCPServer) getClusterInfo(ctx context.Context) (interface{}, error) {
return nil, fmt.Errorf("failed to get cluster version: %v", err)
}
return map[string]string{
data, err := json.Marshal(map[string]string{
"version": version.String(),
"platform": version.Platform,
"gitVersion": version.GitVersion,
}, nil
}
// handleSSE handles Server-Sent Events for MCP
func (s *MCPServer) handleSSE(w http.ResponseWriter, r *http.Request) {
// Set headers for SSE
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
// Create a channel to receive messages
msgChan := make(chan string)
defer close(msgChan)
// Start a goroutine to handle the stdio transport
go func() {
// TODO: Implement message handling between HTTP and stdio transport
// This would require implementing a custom transport that bridges HTTP and stdio
}()
// Send messages to the client
for msg := range msgChan {
if _, err := fmt.Fprintf(w, "data: %s\n\n", msg); err != nil {
s.logger.Error("Failed to write SSE message", zap.Error(err))
return
}
w.(http.Flusher).Flush()
}
}
// handleAnalyzeHTTP handles HTTP requests for the analyze endpoint
func (s *MCPServer) handleAnalyzeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse the request body
var req AnalyzeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("Failed to decode request: %v", err), http.StatusBadRequest)
return
}
// Validate MaxConcurrency to prevent excessive memory allocation
req.MaxConcurrency = validateMaxConcurrency(req.MaxConcurrency)
// Call the analyze handler
resp, err := s.handleAnalyze(r.Context(), &req)
})
if err != nil {
http.Error(w, fmt.Sprintf("Failed to analyze: %v", err), http.StatusInternalServerError)
return
}
// Set response headers
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Write the response
if err := json.NewEncoder(w).Encode(resp); err != nil {
s.logger.Error("Failed to encode response", zap.Error(err))
return mcp_golang.NewResourceResponse(
mcp_golang.NewTextEmbeddedResource(
"cluster-info",
"Failed to marshal cluster info",
"text/plain",
),
), nil
}
return mcp_golang.NewResourceResponse(
mcp_golang.NewTextEmbeddedResource(
"cluster-info",
string(data),
"application/json",
),
), nil
}
// Close closes the MCP server and releases resources

View File

@@ -1,11 +1,14 @@
package server
import (
"bytes"
"context"
"net"
"net/http"
"testing"
"time"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"google.golang.org/grpc"
@@ -14,7 +17,12 @@ import (
func TestServe(t *testing.T) {
logger, _ := zap.NewDevelopment()
defer logger.Sync()
defer func() {
err := logger.Sync()
if err != nil {
t.Logf("logger.Sync() error: %v", err)
}
}()
s := &Config{
Port: "50059",
@@ -34,7 +42,11 @@ func TestServe(t *testing.T) {
conn, err := grpc.Dial("localhost:50059", grpc.WithInsecure())
assert.NoError(t, err, "Should be able to dial the server")
defer conn.Close()
defer func() {
if err := conn.Close(); err != nil {
t.Logf("failed to close connection: %v", err)
}
}()
// Test a simple gRPC reflection request
cli := grpc_reflection_v1alpha.NewServerReflectionClient(conn)
@@ -49,12 +61,172 @@ func TestServe(t *testing.T) {
assert.NoError(t, err, "Shutdown should not return an error")
}
// TestMCPServerCreation tests the creation of an MCP server
func TestMCPServerCreation(t *testing.T) {
logger, _ := zap.NewDevelopment()
defer func() {
err := logger.Sync()
if err != nil {
t.Logf("logger.Sync() error: %v", err)
}
}()
aiProvider := &ai.AIProvider{
Name: "test-provider",
Password: "test-password",
Model: "test-model",
}
// Test HTTP mode
mcpServer, err := NewMCPServer("8089", aiProvider, true, logger)
assert.NoError(t, err, "Should be able to create MCP server with HTTP transport")
assert.NotNil(t, mcpServer, "MCP server should not be nil")
assert.True(t, mcpServer.useHTTP, "MCP server should be in HTTP mode")
assert.Equal(t, "8089", mcpServer.port, "Port should be set correctly")
// Test stdio mode
mcpServerStdio, err := NewMCPServer("8089", aiProvider, false, logger)
assert.NoError(t, err, "Should be able to create MCP server with stdio transport")
assert.NotNil(t, mcpServerStdio, "MCP server should not be nil")
assert.False(t, mcpServerStdio.useHTTP, "MCP server should be in stdio mode")
}
// TestMCPServerBasicHTTP tests basic HTTP connectivity to the MCP server
func TestMCPServerBasicHTTP(t *testing.T) {
logger, _ := zap.NewDevelopment()
defer func() {
err := logger.Sync()
if err != nil {
t.Logf("logger.Sync() error: %v", err)
}
}()
aiProvider := &ai.AIProvider{
Name: "test-provider",
Password: "test-password",
Model: "test-model",
}
mcpServer, err := NewMCPServer("8089", aiProvider, true, logger)
assert.NoError(t, err, "Should be able to create MCP server")
// Start the MCP server in a goroutine
go func() {
err := mcpServer.Start()
// Note: Start() might return an error when the server is stopped, which is expected
if err != nil {
logger.Info("MCP server stopped", zap.Error(err))
}
}()
// Wait for the server to start
err = waitForPort("localhost:8089", 10*time.Second)
if err != nil {
t.Skipf("MCP server did not start within timeout: %v", err)
}
// Test basic connectivity to the MCP endpoint
// The MCP HTTP transport uses a single POST endpoint for all requests
resp, err := http.Post("http://localhost:8089/mcp", "application/json", bytes.NewBufferString(`{"jsonrpc":"2.0","id":1,"method":"tools/list"}`))
if err != nil {
t.Logf("MCP endpoint test skipped (server might not be fully ready): %v", err)
return
}
defer func() {
err := resp.Body.Close()
if err != nil {
t.Logf("resp.Body.Close() error: %v", err)
}
}()
// Accept both 200 and 404 as valid responses (404 means endpoint not implemented)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
t.Errorf("MCP endpoint returned unexpected status: %d", resp.StatusCode)
}
// Cleanup
err = mcpServer.Close()
assert.NoError(t, err, "MCP server should close without error")
}
// TestMCPServerToolCall tests calling a specific tool (analyze) through the MCP server
func TestMCPServerToolCall(t *testing.T) {
logger, _ := zap.NewDevelopment()
defer func() {
err := logger.Sync()
if err != nil {
t.Logf("logger.Sync() error: %v", err)
}
}()
aiProvider := &ai.AIProvider{
Name: "test-provider",
Password: "test-password",
Model: "test-model",
}
mcpServer, err := NewMCPServer("8090", aiProvider, true, logger)
assert.NoError(t, err, "Should be able to create MCP server")
// Start the MCP server in a goroutine
go func() {
err := mcpServer.Start()
if err != nil {
logger.Info("MCP server stopped", zap.Error(err))
}
}()
// Wait for the server to start
err = waitForPort("localhost:8090", 10*time.Second)
if err != nil {
t.Skipf("MCP server did not start within timeout: %v", err)
}
// Test calling the analyze tool with proper JSON-RPC format
analyzeRequest := `{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "analyze",
"arguments": {
"namespace": "default",
"backend": "openai",
"language": "english",
"explain": true,
"maxConcurrency": 10
}
}
}`
resp, err := http.Post("http://localhost:8090/mcp", "application/json", bytes.NewBufferString(analyzeRequest))
if err != nil {
t.Logf("Analyze tool call test skipped (server might not be fully ready): %v", err)
return
}
defer func() {
err := resp.Body.Close()
if err != nil {
t.Logf("resp.Body.Close() error: %v", err)
}
}()
// Accept both 200 and 404 as valid responses
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
t.Errorf("Analyze tool call returned unexpected status: %d", resp.StatusCode)
}
// Cleanup
err = mcpServer.Close()
assert.NoError(t, err, "MCP server should close without error")
}
func waitForPort(address string, timeout time.Duration) error {
start := time.Now()
for {
conn, err := net.Dial("tcp", address)
if err == nil {
conn.Close()
_ = conn.Close()
return nil
}
if time.Since(start) > timeout {