Compare commits

..

65 Commits

Author SHA1 Message Date
renovate[bot]
c353aaa3a3 chore(deps): update anchore/sbom-action action to v0.17.4 (#1273)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
3e62c74d4e fix(deps): update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.4.1 (#1275)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
f6330d0c30 fix(deps): update module buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go to v1.35.1-20240920204244-7a91c8620515.1 (#1274)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
c941e73afd fix(deps): update module github.com/adrg/xdg to v0.5.0 (#1262)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
881fd55bc7 fix(deps): update module cloud.google.com/go/storage to v1.44.0 (#1265)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
090611cb39 chore(deps): update actions/checkout digest to eef6144 (#1270)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
49d14e1e75 fix(deps): update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.8.0 (#1264)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
4e0ccc8195 chore(deps): update module github.com/docker/docker to v27.3.1+incompatible (#1225)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
55bdff7cf8 fix(deps): update module github.com/aws/aws-sdk-go to v1.55.5 (#1263)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
5a886c1de4 fix(deps): update module github.com/aquasecurity/trivy-operator to v0.22.0 (#1034)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Naveen Thangaraj
b7a14f98cb feat: error from events for STS analyzer (#1256)
* error from events for sts analyzer

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

* Signedoff this commit

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

---------

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>
Co-authored-by: Aris Boutselis <arisboutselis08@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
9ee2fc4084 fix(deps): update k8s.io/utils digest to 49e7df5 (#1259)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
ca46793c0d chore(deps): update anchore/sbom-action action to v0.17.2 (#1248)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Yanwei Li
ff73434d34 fix: add providerId to serve mode (#1260)
Signed-off-by: yanweili <yanweili@ibm.com>
Co-authored-by: yanweili <yanweili@ibm.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
b1528c77a0 chore(deps): update golang docker tag to v1.23 (#1254)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Guangya Liu
8e4f56ae28 fix: rename watsonxai to ibmwatsonxai (#1234)
Signed-off-by: Guangya Liu <gyliu@ibm.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
github-actions[bot]
461f715fb8 chore(main): release 0.3.41 (#1217)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Alex Jones
1dea6dc749 feat: adding a query mode for the schednex scheduler (#1257)
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
ce5ade86b8 fix(deps): update module github.com/mittwald/go-helm-client to v0.12.13 (#1251)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Alex Jones
9e75f4aed5 Update README.md (#1253)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
69b92680d9 chore(deps): update actions/upload-artifact digest to 5076954 (#1239)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
b1424e1bf0 fix(deps): update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 to v2.22.0-20240807134501-ea98c104104d.1 (#1186)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
awsyshah
91fafeb1f2 feat: added support for A21 and Amazon Titan models via bedrock api (#1101)
* feat: added support for A21 and Amazon Titan models via bedrock api

Signed-off-by: Yomesh Shah <yomesh@gmail.com>

* fix: response type for diffrent models and use of constant for top_P

Signed-off-by: Yomesh Shah <yomesh@gmail.com>

* fix: constant for top_P as int vs string

Signed-off-by: Yomesh Shah <yomesh@gmail.com>

* feat: moved topP and maxTokens to config rather than being constants in the code

Signed-off-by: Yomesh Shah <yomesh@gmail.com>

---------

Signed-off-by: Yomesh Shah <yomesh@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
a7e1aedba9 fix(deps): update k8s.io/utils digest to 702e33f (#1246)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Aris Boutselis
fc2ea26f2e fix: typo (#1244)
Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
911ef1b8cb fix(deps): update module github.com/schollz/progressbar/v3 to v3.15.0 (#1227)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
ac68e457cb chore(deps): update dependency go to v1.23.1 (#1176)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Dominik Augustin
a0b2884f13 fix: helm chart security context rendering if empty (#1235)
Signed-off-by: Dominik Augustin <botchk@users.noreply.github.com>
Co-authored-by: Dominik Augustin <botchk@users.noreply.github.com>
Co-authored-by: Aris Boutselis <arisboutselis08@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
e6e1bbaf3f chore(deps): update docker/login-action digest to 9780b0c (#1212)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
michael12312
7110931ce5 fix: issue-1168, remove duplicate CVE (#1230)
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Guangya Liu
e3e2ba6e50 fix: disable adding multiple openai provider (#1191)
Signed-off-by: Guangya Liu <gyliu@ibm.com>
Co-authored-by: Matthis <matthish29@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Guangya Liu
2fa925bded fix: enabled auth add support watsonx backend (#1190)
Signed-off-by: Guangya Liu <gyliu@ibm.com>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Matthis <matthish29@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
3f070e4ed0 fix(deps): update module github.com/mittwald/go-helm-client to v0.12.12 (#1226)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
3c4cd623d9 chore(deps): update anchore/sbom-action action to v0.17.1 (#1224)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Alex Jones
803806e747 feat: refactoring to the new schema (#1219)
* feat: refactoring to the new schema

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

* chore: updated readme with grpc commands

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

* chore: updated deps

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

---------

Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
Co-authored-by: Matthis <matthish29@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
92ecb4dcf1 fix(deps): update module github.com/docker/docker to v27.1.1+incompatible [security] (#1220)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Xuetao Song
7919d35880 feat: add event failure handling in service analyzer (#1132)
Signed-off-by: magicsong <songxuetao@bytedance.com>

Co-authored-by: magicsong <songxuetao@bytedance.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
8678ced9fa chore(deps): update actions/checkout digest to 692973e (#1129)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Matthis
f50440fb9f feat: add custom-analyzer cmd (#1207)
* feat: add custom analyzer management capability

Introduced the ability to manage custom analyzers in the K8sGPT application, enabling users to add, deploy, and configure custom analyzers from various sources. This enhancement supports extending the application's analytical capabilities by integrating external analysis tools, thus offering more flexibility and customization options to meet specific user needs.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* feat: enhance custom analyzer management with removal functionality

Introduced the ability to remove custom analyzers, streamlining the management process and ensuring flexibility in custom analyzer configuration. This enhancement addresses the need for dynamic customization and maintenance of analyzer setups, facilitating easier updates and modifications to the analysis environment.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* feat: add list command to customAnalyzer for displaying configured analyzers

Implemented a new list command within the customAnalyzer module to enable users to view all configured custom analyzers. This enhancement aims to improve usability by providing a straightforward method for users to inspect their custom analyzer configurations directly from the command line.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* feat: add support for listing, adding, and removing custom analyzers

This update introduces commands to manage custom analyzers in the k8sgpt tool, enhancing flexibility and control over analyzer configurations without the need for direct installation or docker dependency.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* feat: support private docker image authentication for custom analyzers

Added authentication support for pulling private Docker images when adding custom analyzers, enhancing security and access control.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* feat: remove Docker custom analyzer installation

Removed the installation and deployment functionality for custom analyzers, streamlining the process of adding analyzers. This change focuses on simplifying the configuration by eliminating the need for specifying installation types, package URLs, and authentication details for Docker images. The goal is to enhance user experience by making the addition of custom analyzers more straightforward and less error-prone.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* fix: remove unused packageUrl

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* feat: update add command description to reflect broader functionality

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* feat: Add name validation for custom analyzer creation

To ensure the integrity and consistency of analyzer names, we introduced a validation step that checks the format of the name against a predefined regex pattern. This change aims to prevent the creation of analyzers with invalid names, enhancing the system's reliability and usability.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

* feat: refactor customAnalyzer package for consistent naming

Refactored the customAnalyzer package and its references to use consistent snake_case naming for improved code readability and alignment with Go naming conventions.

Signed-off-by: Matthis Holleville <matthish29@gmail.com>

---------

Signed-off-by: Matthis Holleville <matthish29@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Bo Wong
fd08097761 docs: update "CLI Installation" section in README.md (#1126) (#1127)
Signed-off-by: Bo Wang <wangbob@uniontech.com>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
R0CKSTAR
bdb0f2c26b fix: set logger for controller-runtime (#1211)
Signed-off-by: Xiaodong Ye <yeahdongcn@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
renovate[bot]
c2592e3efe chore(deps): update actions/upload-artifact digest to 834a144 (#1214)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
Kay Yan
e8ee090977 fix: segmentation violation during serve (#1215)
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
github-actions[bot]
e0a7af321d chore(main): release 0.3.40 (#1202)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:34 +01:00
AlexsJones
278473bd9d chore: merge
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:30:30 +01:00
renovate[bot]
acd77c1b9a fix(deps): update module github.com/schollz/progressbar/v3 to v3.14.5 (#1145)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
renovate[bot]
cddc7402dc chore(deps): update anchore/sbom-action action to v0.17.0 (#1197)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
Yanwei Li
7e3d452cdb fix: add default maxToken value of watsonxai backend (#1209)
Signed-off-by: yanweili <yanweili@ibm.com>
Co-authored-by: yanweili <yanweili@ibm.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
Naveen Thangaraj
91fb2126bc feat: getting the error from status field for HPA analyzer (#1164)
* getting the error from status field for hpa analyzer

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

* Signed-off

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

* Updated the hpaAnalyzer_test.go

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

* Updated hpa_test.go

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

* Updated the hpa

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

* Updated the Conflicts

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

* Resolved all the conflicts

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

* Updated the test case

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>

---------

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Matthis <matthish29@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
renovate[bot]
d67ec85081 fix(deps): update module cloud.google.com/go/storage to v1.43.0 (#1198)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
Matthis
cc9c05dcb0 feat: custom analysis paralelism (#1203)
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
Guangya Liu
b455119286 fix: auth update throw out exception (#1193)
Signed-off-by: Guangya Liu <gyliu@ibm.com>
Co-authored-by: Matthis <matthish29@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
github-actions[bot]
c67097b372 chore(main): release 0.3.39 (#1181)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
JuHyung Son
58a76a94d8 feat: add label selector (#1201)
* feat: fix the custom-analysis printing (#1195)

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

* feat: add label selector

Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

* feat: add label selector on analyzers

Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

* chore(deps): pin goreleaser/goreleaser-action action to 286f3b1 (#1171)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

* fix(deps): update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.4.0-20240715142657-3785f0a44aae.2 (#1196)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

* chore(deps): update actions/upload-artifact digest to 0b2256b (#1175)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

* chore: update proto pkg version

Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

* chore: fix typo

Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

* feat: add label string to LabelSelector util func

Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

* feat: add test using 2 label selector

Signed-off-by: JuHyung-Son <sonju0427@gmail.com>

---------

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: JuHyung-Son <sonju0427@gmail.com>
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: JuHyung Son <sonju0427@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
Ronald Petty
6ef0e7dd8e feat: initial kyverno support (#1200)
* refresh fork

Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* let out, wasn't sure it was needed, but seems it might be in CI pipeline

Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* chore(deps): update docker/setup-buildx-action digest to 4fd8129 (#1173)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* fix(deps): update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 to v2.20.0-20240406062209-1cc152efbf5c.1 (#1147)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* chore(deps): update anchore/sbom-action action to v0.16.0 (#1146)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* fix(deps): update module google.golang.org/grpc to v1.64.1 [security] (#1178)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* fix(deps): update module github.com/mittwald/go-helm-client to v0.12.10 (#1177)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* feat: add custom http headers to openai related api backends (#1174)

* feat: add custom http headers to openai related api backends

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* ci: add custom headers test

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* add error handling

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* chore(deps): update docker/setup-buildx-action digest to 4fd8129 (#1173)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* fix(deps): update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 to v2.20.0-20240406062209-1cc152efbf5c.1 (#1147)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* chore(deps): update anchore/sbom-action action to v0.16.0 (#1146)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

* Update README.md

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>

---------

Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* chore(main): release 0.3.38 (#1165)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* chore(deps): update anchore/sbom-action action to v0.16.1 (#1179)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* chore(deps): update actions/setup-go digest to 0a12ed9 (#1182)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* ci: release to krew index as kubectl-gpt (#1100)

Signed-off-by: Miguel Varela Ramos <miguel@cohere.ai>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* fix(deps): update k8s.io/utils digest to 18e509b (#1183)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* fix(deps): update module github.com/prometheus/prometheus to v0.53.1 (#1035)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* fix(deps): update module github.com/ibm/watsonx-go to v1.0.1 (#1187)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* feat: skip k3s node type EtcdIsVoter (#1167)

Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Co-authored-by: Aris Boutselis <arisboutselis08@gmail.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* feat: fix the custom-analysis printing (#1195)

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* chore(deps): pin goreleaser/goreleaser-action action to 286f3b1 (#1171)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* fix(deps): update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.4.0-20240715142657-3785f0a44aae.2 (#1196)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* chore(deps): update actions/upload-artifact digest to 0b2256b (#1175)

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* Update pkg/integration/kyverno/kyverno.go

lgtm

Co-authored-by: Alex Jones <alex@k8sgpt.ai>
Signed-off-by: Ronald Petty <ronald.petty@minimumdistance.com>
Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

* remove unused function

Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>

---------

Signed-off-by: Ronald Petty <ronald.petty@rx-m.com>
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Aris Boutselis <arisboutselis08@gmail.com>
Signed-off-by: Miguel Varela Ramos <miguel@cohere.ai>
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: Ronald Petty <ronald.petty@minimumdistance.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Aris Boutselis <arisboutselis08@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Miguel Varela Ramos <miguel@cohere.ai>
Co-authored-by: Kay Yan <kay.yan@daocloud.io>
Co-authored-by: Alex Jones <alex@k8sgpt.ai>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
renovate[bot]
0e330a4248 chore(deps): update actions/upload-artifact digest to 0b2256b (#1175)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
renovate[bot]
5cfa2bc0ab fix(deps): update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.4.0-20240715142657-3785f0a44aae.2 (#1196)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
renovate[bot]
185a07d30a chore(deps): pin goreleaser/goreleaser-action action to 286f3b1 (#1171)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
Alex Jones
096f43356d feat: fix the custom-analysis printing (#1195)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
Kay Yan
4e125f1692 feat: skip k3s node type EtcdIsVoter (#1167)
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Co-authored-by: Aris Boutselis <arisboutselis08@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
renovate[bot]
0dffc5be1e fix(deps): update module github.com/ibm/watsonx-go to v1.0.1 (#1187)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
renovate[bot]
5a03e253fd fix(deps): update module github.com/prometheus/prometheus to v0.53.1 (#1035)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
renovate[bot]
5d83d0d402 fix(deps): update k8s.io/utils digest to 18e509b (#1183)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
jiajia
aebe9680da fix:config nil pointer
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
jiajia
84a548a304 fix:config nil point
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2024-10-24 07:27:47 +01:00
128 changed files with 3957 additions and 12396 deletions

View File

@@ -8,16 +8,13 @@ on:
pull_request:
branches:
- 'main'
- fix/build-branch
- '[0-9]+.[1-9][0-9]*.x'
paths-ignore:
- "**.md"
env:
GO_VERSION: "~1.24"
GO_VERSION: "~1.22"
IMAGE_NAME: "k8sgpt"
REGISTRY_IMAGE: ghcr.io/k8sgpt-ai/k8sgpt
defaults:
run:
shell: bash
@@ -25,7 +22,7 @@ defaults:
jobs:
prepare_ci_run:
name: Prepare CI Run
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
outputs:
GIT_SHA: ${{ steps.extract_branch.outputs.GIT_SHA }}
BRANCH: ${{ steps.extract_branch.outputs.BRANCH }}
@@ -36,7 +33,7 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Extract branch name
id: extract_branch
@@ -54,61 +51,97 @@ 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-and-push:
name: Build and Push Multi-arch Image
build_image:
name: Build Container Image
needs: prepare_ci_run
runs-on: ubuntu-latest
if: ${{ needs.prepare_ci_run.outputs.NON_FORKED_AND_NON_ROBOT_RUN == 'true' }}
runs-on: ubuntu-22.04
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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Docker meta
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
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@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.K8SGPT_BOT_SECRET }}
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
id: buildx
uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3
- name: Build and push multi-arch image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
- name: Build Docker Image
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: .
platforms: linux/amd64
file: ./container/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
target: production
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@50769540e7f4bd5e21e526ee35c689e35e0d6874 # 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-22.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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3
- name: Build Docker Image
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: .
file: ./container/Dockerfile
platforms: linux/amd64,linux/arm64
target: production
tags: |
${{ env.REGISTRY_IMAGE }}:${{ env.DATETIME }}
labels: ${{ steps.meta.outputs.labels }}
secrets: |
GIT_AUTH_TOKEN=${{ secrets.K8SGPT_BOT_SECRET }}
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 }}

View File

@@ -9,10 +9,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
uses: reviewdog/action-golangci-lint@7708105983c614f7a2725e2172908b7709d1c3e4 # v2
with:
version: v2.11.3
only-new-issues: true
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-check
golangci_lint_flags: "--timeout=240s"
level: warning

View File

@@ -23,7 +23,7 @@ jobs:
# Release-please creates a PR that tracks all changes
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1
id: release
@@ -40,32 +40,18 @@ jobs:
- release-please
runs-on: ubuntu-latest
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: false
dotnet: false
haskell: false
large-packages: true
docker-images: true
swap-storage: true
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5
with:
go-version: '~1.26.0'
go-version: '1.22'
- name: Download Syft
uses: anchore/sbom-action/download-syft@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
uses: anchore/sbom-action/download-syft@8d0a6505bf28ced3e85154d13dc6af83299e13f1 # v0.17.4
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6
uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
@@ -73,15 +59,14 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.K8SGPT_BOT_SECRET }}
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
# - name: Update new version in krew-index
# uses: rajatjindal/krew-release-bot@3d9faef30a82761d610544f62afddca00993eef9 # v0.0.47
- name: Update new version in krew-index
uses: rajatjindal/krew-release-bot@df3eb197549e3568be8b4767eec31c5e8e8e6ad8 # v0.0.46
build-container:
if: needs.release-please.outputs.releases_created == 'true'
needs:
- release-please
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
permissions:
contents: write
packages: write
@@ -91,23 +76,23 @@ jobs:
IMAGE_NAME: k8sgpt
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
submodules: recursive
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3
- name: Login to GitHub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.K8SGPT_BOT_SECRET }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: .
file: ./container/Dockerfile
@@ -121,14 +106,14 @@ jobs:
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_TAG }}
- name: Generate SBOM
uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
uses: anchore/sbom-action@8d0a6505bf28ced3e85154d13dc6af83299e13f1 # v0.17.4
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@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
tag_name: ${{ needs.release-please.outputs.tag_name }}
files: ./sbom-${{ env.IMAGE_NAME }}.spdx.json

View File

@@ -10,13 +10,13 @@ defaults:
shell: bash
jobs:
validate:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
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@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -9,22 +9,22 @@ on:
- main
env:
GO_VERSION: "~1.24"
GO_VERSION: "~1.22"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # 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@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

1
.gitignore vendored
View File

@@ -7,4 +7,3 @@ k8sgpt*
dist/
bin/
pkg/server/example/example

View File

@@ -70,28 +70,8 @@ checksum:
snapshot:
name_template: "{{ incpatch .Version }}-next"
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: ""
# 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

View File

@@ -1 +1 @@
{".":"0.4.30"}
{".":"0.3.41"}

View File

@@ -1,586 +1,5 @@
# 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)
### Features
* free disk ([#1313](https://github.com/k8sgpt-ai/k8sgpt/issues/1313)) ([783cd1c](https://github.com/k8sgpt-ai/k8sgpt/commit/783cd1cfc66f8e4489e5006529745d8caf38cfd4))
## [0.3.44](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.43...v0.3.44) (2024-11-09)
### Features
* test revert runner on release job ([#1310](https://github.com/k8sgpt-ai/k8sgpt/issues/1310)) ([cc9b3ea](https://github.com/k8sgpt-ai/k8sgpt/commit/cc9b3ea6579c6190629e0fac48e37e0eba650158))
## [0.3.43](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.42...v0.3.43) (2024-11-05)
### Bug Fixes
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1296](https://github.com/k8sgpt-ai/k8sgpt/issues/1296)) ([2f75986](https://github.com/k8sgpt-ai/k8sgpt/commit/2f759865b6fc5ae143c8f5e89a306abc89d4de27))
### Other
* **deps:** update dependency ubuntu to v24 ([#1293](https://github.com/k8sgpt-ai/k8sgpt/issues/1293)) ([c67add3](https://github.com/k8sgpt-ai/k8sgpt/commit/c67add30c64257ac6258dec93193e3201ba8c4ab))
## [0.3.42](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.41...v0.3.42) (2024-11-04)
### Features
* add stats option to analyze command for performance insights ([#1237](https://github.com/k8sgpt-ai/k8sgpt/issues/1237)) ([3eec9bb](https://github.com/k8sgpt-ai/k8sgpt/commit/3eec9bbb05b2f0717437cc4a2ec786594ece1cc3))
* error from events for STS analyzer ([#1256](https://github.com/k8sgpt-ai/k8sgpt/issues/1256)) ([d8fad95](https://github.com/k8sgpt-ai/k8sgpt/commit/d8fad956f45a4dd668647379bb0295e169faeac6))
### Bug Fixes
* [Bug] Make lint command is not working ([#1282](https://github.com/k8sgpt-ai/k8sgpt/issues/1282)) ([87565a0](https://github.com/k8sgpt-ai/k8sgpt/commit/87565a0bcce7087114798c3a32877894c8a9dcee))
* add providerId to serve mode ([#1260](https://github.com/k8sgpt-ai/k8sgpt/issues/1260)) ([da0764d](https://github.com/k8sgpt-ai/k8sgpt/commit/da0764d951ca76cb7007c412f8efa794619c20ba))
* **deps:** update all non-major dependencies ([#1291](https://github.com/k8sgpt-ai/k8sgpt/issues/1291)) ([14e0f19](https://github.com/k8sgpt-ai/k8sgpt/commit/14e0f19b12189052b03d551e409b407fd0b6bd30))
* **deps:** update k8s.io/utils digest to 49e7df5 ([#1259](https://github.com/k8sgpt-ai/k8sgpt/issues/1259)) ([7785dd1](https://github.com/k8sgpt-ai/k8sgpt/commit/7785dd12a0245a33af25dedd2fbb5f4178b5cda9))
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go to v1.35.1-20240920204244-7a91c8620515.1 ([#1274](https://github.com/k8sgpt-ai/k8sgpt/issues/1274)) ([9f39abf](https://github.com/k8sgpt-ai/k8sgpt/commit/9f39abf89e4e92009f5e138d9b01d11c60ac135c))
* **deps:** update module cloud.google.com/go/storage to v1.44.0 ([#1265](https://github.com/k8sgpt-ai/k8sgpt/issues/1265)) ([4143e9f](https://github.com/k8sgpt-ai/k8sgpt/commit/4143e9fd524bed3179524d949b7b0f92c02ecd11))
* **deps:** update module github.com/adrg/xdg to v0.5.0 ([#1262](https://github.com/k8sgpt-ai/k8sgpt/issues/1262)) ([98237b6](https://github.com/k8sgpt-ai/k8sgpt/commit/98237b6408521ee7afc05fcaed2f78ba79e77144))
* **deps:** update module github.com/aquasecurity/trivy-operator to v0.22.0 ([#1034](https://github.com/k8sgpt-ai/k8sgpt/issues/1034)) ([037e745](https://github.com/k8sgpt-ai/k8sgpt/commit/037e745c6f667830f0e1d531ce4bbd07083ef972))
* **deps:** update module github.com/aws/aws-sdk-go to v1.55.5 ([#1263](https://github.com/k8sgpt-ai/k8sgpt/issues/1263)) ([0148a5b](https://github.com/k8sgpt-ai/k8sgpt/commit/0148a5b3549cbdb6c6e5832dc01aab044b90ddc9))
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/azidentity to v1.8.0 ([#1264](https://github.com/k8sgpt-ai/k8sgpt/issues/1264)) ([3613585](https://github.com/k8sgpt-ai/k8sgpt/commit/36135857ac55e126b3a6c4533a000cb0b7f32c6b))
* **deps:** update module github.com/azure/azure-sdk-for-go/sdk/storage/azblob to v1.4.1 ([#1275](https://github.com/k8sgpt-ai/k8sgpt/issues/1275)) ([c9b11b6](https://github.com/k8sgpt-ai/k8sgpt/commit/c9b11b6eee00d0269a4d48ad2e4be5458436b51d))
* **deps:** update module github.com/cohere-ai/cohere-go/v2 to v2.12.0 ([#1276](https://github.com/k8sgpt-ai/k8sgpt/issues/1276)) ([7a3fb3c](https://github.com/k8sgpt-ai/k8sgpt/commit/7a3fb3cf6777d5b0babf00455c3833a47bb1bfdb))
* **deps:** update module github.com/google/generative-ai-go to v0.18.0 ([#1278](https://github.com/k8sgpt-ai/k8sgpt/issues/1278)) ([ad349ae](https://github.com/k8sgpt-ai/k8sgpt/commit/ad349ae263f226e300f60dd092729c5a3bf61dbe))
* rename watsonxai to ibmwatsonxai ([#1234](https://github.com/k8sgpt-ai/k8sgpt/issues/1234)) ([5ff6dc9](https://github.com/k8sgpt-ai/k8sgpt/commit/5ff6dc9be5218e47839c4ac5e8f3458b40eb9c88))
### Other
* **deps:** update actions/checkout digest to 11bd719 ([#1283](https://github.com/k8sgpt-ai/k8sgpt/issues/1283)) ([0cfecbd](https://github.com/k8sgpt-ai/k8sgpt/commit/0cfecbdd87586fd138cc63c4e7a26d54e7ed83a8))
* **deps:** update actions/checkout digest to eef6144 ([#1270](https://github.com/k8sgpt-ai/k8sgpt/issues/1270)) ([72eb815](https://github.com/k8sgpt-ai/k8sgpt/commit/72eb8159fb4a2284cf43eb6a5f3de7bed10c6224))
* **deps:** update actions/upload-artifact digest to b4b15b8 ([#1272](https://github.com/k8sgpt-ai/k8sgpt/issues/1272)) ([911d578](https://github.com/k8sgpt-ai/k8sgpt/commit/911d578bf006253d10fe21d96888ddf34a8b4691))
* **deps:** update anchore/sbom-action action to v0.17.2 ([#1248](https://github.com/k8sgpt-ai/k8sgpt/issues/1248)) ([04582d8](https://github.com/k8sgpt-ai/k8sgpt/commit/04582d85160055da30e4e00fd3c6ca69d1decd1a))
* **deps:** update anchore/sbom-action action to v0.17.4 ([#1273](https://github.com/k8sgpt-ai/k8sgpt/issues/1273)) ([c128bf7](https://github.com/k8sgpt-ai/k8sgpt/commit/c128bf7942e380fcab5e9771f405471198e388fe))
* **deps:** update anchore/sbom-action action to v0.17.6 ([#1285](https://github.com/k8sgpt-ai/k8sgpt/issues/1285)) ([173e4dc](https://github.com/k8sgpt-ai/k8sgpt/commit/173e4dc5ac6265af4a3538556220d3a43ab721f7))
* **deps:** update codecov/codecov-action action to v4 ([#1292](https://github.com/k8sgpt-ai/k8sgpt/issues/1292)) ([c1a38c2](https://github.com/k8sgpt-ai/k8sgpt/commit/c1a38c2b35a0bfa772b88f15843c9354b0345284))
* **deps:** update docker/setup-buildx-action digest to c47758b ([#1213](https://github.com/k8sgpt-ai/k8sgpt/issues/1213)) ([161bc11](https://github.com/k8sgpt-ai/k8sgpt/commit/161bc11294d5094533068cf7af9880795a61536e))
* **deps:** update golang docker tag to v1.23 ([#1254](https://github.com/k8sgpt-ai/k8sgpt/issues/1254)) ([b62b7db](https://github.com/k8sgpt-ai/k8sgpt/commit/b62b7dbe3c9cd02b81f6a0111bca939034c5cc9f))
* **deps:** update module github.com/docker/docker to v27.3.1+incompatible ([#1225](https://github.com/k8sgpt-ai/k8sgpt/issues/1225)) ([9c1927b](https://github.com/k8sgpt-ai/k8sgpt/commit/9c1927b4975fa8132fbc24dd96a5737819855544))
* renovate.json ([#1290](https://github.com/k8sgpt-ai/k8sgpt/issues/1290)) ([458fcfe](https://github.com/k8sgpt-ai/k8sgpt/commit/458fcfe8d330523781d32af680febc2a0c0525a2))
## [0.3.41](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.40...v0.3.41) (2024-09-22)

View File

@@ -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.24+`
- Golang `1.20`
- 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
View File

@@ -1,482 +0,0 @@
# 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)

View File

@@ -85,12 +85,6 @@ 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:
@@ -104,7 +98,7 @@ vet:
## lint: Run go lint against code.
.PHONY: lint
lint:
@golangci-lint run -v --timeout=5m ./...
@golangci-lint run -v ./...
## style: Code style -> fmt,vet,lint
.PHONY: style

214
README.md
View File

@@ -21,35 +21,17 @@ 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&#0032;gives&#0032;Kubernetes&#0032;Superpowers&#0032;to&#0032;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="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<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)
<img src="images/demo4.gif" width=650px; />
# CLI Installation
### Linux/Mac via brew
```sh
brew install k8sgpt
$ brew install k8sgpt
```
or
@@ -67,7 +49,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.rpm
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.41/k8sgpt_386.rpm
```
<!---x-release-please-end-->
@@ -75,7 +57,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.rpm
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.41/k8sgpt_amd64.rpm
```
<!---x-release-please-end-->
</details>
@@ -88,7 +70,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.41/k8sgpt_386.deb
sudo dpkg -i k8sgpt_386.deb
```
@@ -99,7 +81,7 @@ sudo dpkg -i k8sgpt_386.deb
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.41/k8sgpt_amd64.deb
sudo dpkg -i k8sgpt_amd64.deb
```
@@ -114,7 +96,7 @@ sudo dpkg -i k8sgpt_amd64.deb
<!---x-release-please-start-version-->
```
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_386.apk
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.41/k8sgpt_386.apk
apk add --allow-untrusted k8sgpt_386.apk
```
<!---x-release-please-end-->
@@ -123,7 +105,7 @@ sudo dpkg -i k8sgpt_amd64.deb
<!---x-release-please-start-version-->
```
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.30/k8sgpt_amd64.apk
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.41/k8sgpt_amd64.apk
apk add --allow-untrusted k8sgpt_amd64.apk
```
<!---x-release-please-end-->
@@ -151,7 +133,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_ environment variable with the binary location
- Extract the downloaded package to your desired location. Configure the system _path_ variable with the binary location
## Operator Installation
@@ -170,76 +152,6 @@ _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
@@ -257,12 +169,10 @@ 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
@@ -273,16 +183,6 @@ 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
@@ -404,26 +304,6 @@ _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_
```
@@ -439,29 +319,6 @@ _Analysis with custom headers_
k8sgpt analyze --explain --custom-headers CustomHeaderKey:CustomHeaderValue
```
_Print analysis stats_
```
k8sgpt analyze -s
The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.
- Analyzer Ingress took 47.125583ms
- Analyzer PersistentVolumeClaim took 53.009167ms
- Analyzer CronJob took 57.517792ms
- Analyzer Deployment took 156.6205ms
- Analyzer Node took 160.109833ms
- Analyzer ReplicaSet took 245.938333ms
- Analyzer StatefulSet took 448.0455ms
- Analyzer Pod took 5.662594708s
- Analyzer Service took 38.583359166s
```
_Diagnostic information_
To collect diagnostic information use the following command to create a `dump_<timestamp>_json` in your local directory.
```
k8sgpt dump
```
</details>
## LLM AI Backends
@@ -486,8 +343,6 @@ Unused:
> huggingface
> noopai
> googlevertexai
> watsonxai
> customrest
> ibmwatsonxai
```
@@ -500,22 +355,6 @@ 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>
@@ -548,9 +387,11 @@ 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
Note: **Anonymization does not currently apply to events.**
**Anonymization does not currently apply to events.**
_In a few analysers like Pod, we feed to the AI backend the event messages which are not known beforehand thus we are not masking them for the **time being**._
@@ -568,7 +409,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**:-
- ReplicaSet
- RepicaSet
- PersistentVolumeClaim
- Pod
- Log
@@ -619,7 +460,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 configure and use only one remote cache at a time</em>
<em>Note: You can only configure and use only one remote cache at a time</em>
_Adding a remote cache_
@@ -665,7 +506,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/custom_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/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:
@@ -704,30 +545,7 @@ 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)
@@ -738,7 +556,7 @@ Please read our [contributing guide](./CONTRIBUTING.md).
## Community
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-332vhyaxv-bfjJwHZLXWVCB3QaXafEYQ)
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-276pa9uyq-pxAUr4TCVHubFxEvLZuT1Q)
<a href="https://github.com/k8sgpt-ai/k8sgpt/graphs/contributors">
<img src="https://contrib.rocks/image?repo=k8sgpt-ai/k8sgpt" />

View File

@@ -1,83 +0,0 @@
# 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.

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: v0.4.23 #x-release-please-version
appVersion: v0.3.0 #x-release-please-version
description: A Helm chart for K8SGPT
name: k8sgpt
type: application

View File

@@ -32,13 +32,7 @@ spec:
image: {{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default .Chart.AppVersion }}
ports:
- containerPort: 8080
{{- 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 }}
]
args: ["serve"]
{{- if .Values.deployment.resources }}
resources:
{{- toYaml .Values.deployment.resources | nindent 10 }}

View File

@@ -19,9 +19,4 @@ 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 }}

View File

@@ -1,39 +0,0 @@
# 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: {}

View File

@@ -7,11 +7,6 @@ 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"

View File

@@ -23,7 +23,6 @@ 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 (
@@ -41,7 +40,6 @@ var (
interactiveMode bool
customAnalysis bool
customHeaders []string
withStats bool
)
// AnalyzeCmd represents the problems command
@@ -65,58 +63,31 @@ var AnalyzeCmd = &cobra.Command{
withDoc,
interactiveMode,
customHeaders,
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 {
err := config.GetAIResults(output, anonymize)
if verbose {
fmt.Println("Debug: Checking AI results.")
}
if err != nil {
if err := config.GetAIResults(output, anonymize); 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)
}
if withStats {
statsData := config.PrintStats()
fmt.Println(string(statsData))
}
fmt.Println(string(output_data))
if interactiveMode && explain {
@@ -175,6 +146,4 @@ func init() {
AnalyzeCmd.Flags().StringSliceVarP(&customHeaders, "custom-headers", "r", []string{}, "Custom Headers, <key>:<value> (e.g CustomHeaderKey:CustomHeaderValue AnotherHeader:AnotherValue)")
// label selector flag
AnalyzeCmd.Flags().StringVarP(&labelSelector, "selector", "L", "", "Label selector (label query) to filter on, supports '=', '==', and '!='. (e.g. -L key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.")
// print stats
AnalyzeCmd.Flags().BoolVarP(&withStats, "with-stat", "s", false, "Print analysis stats. This option disables errors display.")
}

View File

@@ -28,7 +28,7 @@ import (
const (
defaultBackend = "openai"
defaultModel = "gpt-4o"
defaultModel = "gpt-3.5-turbo"
)
var addCmd = &cobra.Command{

View File

@@ -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.", backend)
color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", args[0])
os.Exit(1)
}

10
cmd/cache/add.go vendored
View File

@@ -17,7 +17,6 @@ package cache
import (
"fmt"
"os"
"strings"
"github.com/fatih/color"
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
@@ -41,10 +40,9 @@ 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 (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)`,
- Azure Blob storage
- Google Cloud storage
- S3`,
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")
@@ -52,7 +50,7 @@ var addCmd = &cobra.Command{
}
fmt.Println(color.YellowString("Adding remote based cache"))
cacheType := args[0]
remoteCache, err := cache.NewCacheProvider(strings.ToLower(cacheType), bucketName, region, endpoint, storageAccount, containerName, projectId, insecure)
remoteCache, err := cache.NewCacheProvider(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
View File

@@ -1,45 +0,0 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
View File

@@ -23,51 +23,23 @@ 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 or all objects with --all flag.",
Long: "This command allows you to delete/purge one object from the cache",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
color.Red("Error: Please provide a value for object name. Run k8sgpt cache purge --help")
os.Exit(1)
}
objectKey := args[0]
fmt.Println(color.YellowString("Purging a remote cache."))
c, err := cache.GetCacheConfiguration()
if err != nil {
color.Red("Error: %v", err)
os.Exit(1)
}
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)
@@ -78,6 +50,5 @@ var purgeCmd = &cobra.Command{
}
func init() {
purgeCmd.Flags().BoolVar(&all, "all", false, "Purge all objects in the cache")
CacheCmd.AddCommand(purgeCmd)
}

View File

@@ -1,113 +0,0 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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() {
}

View File

@@ -45,7 +45,7 @@ var GenerateCmd = &cobra.Command{
backendType = backend
}
fmt.Println("")
openbrowser("https://platform.openai.com/api-keys")
openbrowser("https://beta.openai.com/account/api-keys")
},
}
@@ -81,10 +81,10 @@ func openbrowser(url string) {
func printInstructions(isGui bool, backendType string) {
fmt.Println("")
if isGui {
color.Green("Opening: https://platform.openai.com/api-keys to generate a key for %s", backendType)
color.Green("Opening: https://beta.openai.com/account/api-keys to generate a key for %s", backendType)
fmt.Println("")
} else {
color.Green("Please open: https://platform.openai.com/api-keys to generate a key for %s", backendType)
color.Green("Please open: https://beta.openai.com/account/api-keys to generate a key for %s", backendType)
fmt.Println("")
}
color.Green("Please copy the generated key and run `k8sgpt auth add` to add it to your config file")

View 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 prometheus`,
Long: `For example e.g. k8sgpt integration deactivate trivy`,
Run: func(cmd *cobra.Command, args []string) {
integrationName := args[0]

View File

@@ -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 prometheus
k8sgpt integration activate trivy
This would allow you to connect to prometheus running with your cluster.`,
This would allow you to deploy trivy into your cluster and use a K8sGPT analyzer to parse trivy results.`,
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Help()
},

View File

@@ -23,7 +23,6 @@ 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"
@@ -37,7 +36,6 @@ var (
cfgFile string
kubecontext string
kubeconfig string
verbose bool
Version string
Commit string
Date string
@@ -59,9 +57,6 @@ 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)
@@ -75,7 +70,6 @@ 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)
@@ -85,7 +79,6 @@ 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.
@@ -106,7 +99,6 @@ 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

View File

@@ -1,30 +0,0 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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")
}
}

View File

@@ -30,7 +30,6 @@ const (
defaultTemperature float32 = 0.7
defaultTopP float32 = 1.0
defaultTopK int32 = 50
defaultMaxTokens int = 2048
)
var (
@@ -38,11 +37,6 @@ 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{
@@ -108,18 +102,6 @@ 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")
@@ -143,7 +125,6 @@ var ServeCmd = &cobra.Command{
Temperature: temperature(),
TopP: topP(),
TopK: topK(),
MaxTokens: maxTokens(),
}
configAI.Providers = append(configAI.Providers, *aiProvider)
@@ -188,26 +169,6 @@ 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,
@@ -215,7 +176,6 @@ var ServeCmd = &cobra.Command{
EnableHttp: enableHttp,
Token: aiProvider.Password,
Logger: logger,
Filters: filters,
}
go func() {
if err := server.ServeMetrics(); err != nil {
@@ -239,12 +199,7 @@ 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", "m", "8081", "Port to run the metrics-server on (env: K8SGPT_METRICS_PORT)")
ServeCmd.Flags().StringVarP(&metricsPort, "metrics-port", "", "8081", "Port to run the metrics-server on")
ServeCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
ServeCmd.Flags().BoolVarP(&enableHttp, "http", "", false, "Enable REST/http using gppc-gateway")
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)")
}

View File

@@ -9,7 +9,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
FROM golang:1.24-alpine3.23 AS builder
FROM golang:1.23-alpine3.19 AS builder
ENV CGO_ENABLED=0
ARG VERSION

446
go.mod
View File

@@ -1,293 +1,337 @@
module github.com/k8sgpt-ai/k8sgpt
go 1.25.0
go 1.22.3
toolchain go1.26.1
toolchain go1.23.2
require (
github.com/fatih/color v1.18.0
github.com/kedacore/keda/v2 v2.16.0
github.com/magiconair/properties v1.8.10
github.com/mittwald/go-helm-client v0.12.19
github.com/ollama/ollama v0.18.2
github.com/sashabaranov/go-openai v1.41.2
github.com/schollz/progressbar/v3 v3.19.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
golang.org/x/term v0.41.0
helm.sh/helm/v3 v3.19.0
k8s.io/api v0.35.2
k8s.io/apimachinery v0.35.2
k8s.io/client-go v0.35.2
k8s.io/kubectl v0.34.0 // indirect
github.com/aquasecurity/trivy-operator v0.22.0
github.com/fatih/color v1.17.0
github.com/kedacore/keda/v2 v2.11.2
github.com/magiconair/properties v1.8.7
github.com/mittwald/go-helm-client v0.12.13
github.com/ollama/ollama v0.1.48
github.com/sashabaranov/go-openai v1.23.0
github.com/schollz/progressbar/v3 v3.15.0
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.24.0
helm.sh/helm/v3 v3.15.4
k8s.io/api v0.30.3
k8s.io/apimachinery v0.30.3
k8s.io/client-go v0.30.3
k8s.io/kubectl v0.30.3 // indirect
)
require github.com/adrg/xdg v0.5.3
require github.com/adrg/xdg v0.5.0
require (
buf.build/gen/go/interplex-ai/schemas/grpc/go v1.6.1-20241117203254-a91193b62179.1
buf.build/gen/go/interplex-ai/schemas/protocolbuffers/go v1.36.11-20241117203254-a91193b62179.1
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.28.0-20241118152629-1379a5a1889d.1
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.6.1-20241118152629-1379a5a1889d.1
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.36.11-20241118152629-1379a5a1889d.1
cloud.google.com/go/storage v1.61.3
cloud.google.com/go/vertexai v0.17.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
github.com/IBM/watsonx-go v1.0.2
github.com/agiledragon/gomonkey/v2 v2.14.0
github.com/aws/aws-sdk-go v1.55.8
github.com/aws/aws-sdk-go-v2 v1.41.4
github.com/aws/aws-sdk-go-v2/config v1.32.12
github.com/aws/aws-sdk-go-v2/service/bedrock v1.57.0
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.50.2
github.com/aws/smithy-go v1.24.2
github.com/cohere-ai/cohere-go/v2 v2.16.2
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.22.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.44.0
cloud.google.com/go/vertexai v0.7.1
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/IBM/watsonx-go v1.0.1
github.com/aws/aws-sdk-go v1.55.5
github.com/cohere-ai/cohere-go/v2 v2.7.3
github.com/go-logr/zapr v1.3.0
github.com/google/generative-ai-go v0.20.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0
github.com/google/generative-ai-go v0.11.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0
github.com/hupe1980/go-huggingface v0.0.15
github.com/kyverno/policy-reporter-kyverno-plugin v1.6.4
github.com/mark3labs/mcp-go v0.45.0
github.com/kyverno/policy-reporter-kyverno-plugin v1.6.3
github.com/olekukonko/tablewriter v0.0.5
github.com/oracle/oci-go-sdk/v65 v65.109.2
github.com/prometheus/prometheus v0.310.0
github.com/pterm/pterm v0.12.83
google.golang.org/api v0.272.0
github.com/oracle/oci-go-sdk/v65 v65.65.1
github.com/prometheus/prometheus v0.53.1
github.com/pterm/pterm v0.12.79
google.golang.org/api v0.197.0
gopkg.in/yaml.v2 v2.4.0
sigs.k8s.io/controller-runtime v0.23.3
sigs.k8s.io/gateway-api v1.5.1
sigs.k8s.io/controller-runtime v0.18.4
sigs.k8s.io/gateway-api v1.0.0
)
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.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/ai v0.8.0 // indirect
cloud.google.com/go/aiplatform v1.120.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/longrunning v0.8.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
cel.dev/expr v0.16.1 // indirect
cloud.google.com/go v0.115.1 // indirect
cloud.google.com/go/ai v0.3.5-0.20240409161017-ce55ad694f21 // indirect
cloud.google.com/go/aiplatform v1.68.0 // indirect
cloud.google.com/go/auth v0.9.3 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.5.1 // 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.0 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.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.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.12.4 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // 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/service/s3 v1.55.1 // indirect
github.com/aws/smithy-go v1.20.2 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/containerd/console v1.0.5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/errdefs v0.1.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/creack/pty v1.1.21 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/expr-lang/expr v1.17.7 // indirect
github.com/containerd/typeurl/v2 v2.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/go-control-plane v0.13.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/mangling v0.25.4 // indirect
github.com/go-openapi/swag/netutils v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-viper/mapstructure/v2 v2.5.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/gofrs/flock v0.12.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.18.0 // indirect
github.com/gookit/color v1.6.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gorilla/websocket v1.5.2 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-getter v1.7.5 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl/v2 v2.20.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/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/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/client_golang/exp v0.0.0-20260108101519-fb0838f53562 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/sigv4 v0.4.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/prometheus/common/sigv4 v0.1.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/sagikazarmark/locafero v0.4.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.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // 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.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.29.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/tools v0.24.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genai v1.50.0 // indirect
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect
oras.land/oras-go/v2 v2.6.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
knative.dev/pkg v0.0.0-20230616134650-eb63a40adfb0 // indirect
mvdan.cc/sh/v3 v3.8.0 // indirect
)
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Masterminds/squirrel v1.5.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.29 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/containerd/containerd v1.7.18 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/docker/cli v26.1.4+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/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.9.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic v0.7.1
github.com/google/go-cmp v0.7.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gnostic v0.7.0
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-containerregistry v0.19.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.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.18.3 // indirect
github.com/klauspost/compress v1.17.9 // 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.9.1 // indirect
github.com/mattn/go-colorable v0.1.14 // 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.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.2.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.3-0.20250322232337-35a7c28c31ee // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1
github.com/rubenv/sql-migrate v1.8.0 // indirect
github.com/rubenv/sql-migrate v1.6.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.4 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.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.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.starlark.net v0.0.0-20240520160348-046347dcd104 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/grpc v1.79.2
google.golang.org/protobuf v1.36.11 // indirect
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
golang.org/x/net v0.29.0
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/grpc v1.66.2
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/apiextensions-apiserver v0.35.2
k8s.io/apiserver v0.35.2 // indirect
k8s.io/cli-runtime v0.34.0 // indirect
k8s.io/component-base v0.35.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/kustomize/api v0.20.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
k8s.io/apiextensions-apiserver v0.30.3
k8s.io/apiserver v0.30.3 // indirect
k8s.io/cli-runtime v0.30.3 // indirect
k8s.io/component-base v0.30.3 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6
oras.land/oras-go v1.2.5 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.17.2 // indirect
sigs.k8s.io/kustomize/kyaml v0.17.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
// v1.2.0 is taken from github.com/open-policy-agent/opa v0.42.0
// v1.2.0 incompatible with github.com/docker/docker v23.0.0-rc.1+incompatible
//replace oras.land/oras-go => oras.land/oras-go v1.2.4
replace github.com/docker/docker => github.com/docker/docker v28.5.2+incompatible
replace dario.cat/mergo => github.com/imdario/mergo v1.0.2
replace github.com/docker/docker => github.com/docker/docker v27.3.1+incompatible

2826
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -2,20 +2,13 @@ package ai
import (
"context"
"errors"
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
"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"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/bedrockruntime"
)
const amazonbedrockAIClientName = "amazonbedrock"
@@ -24,16 +17,14 @@ const amazonbedrockAIClientName = "amazonbedrock"
type AmazonBedRockClient struct {
nopCloser
client BedrockRuntimeAPI
mgmtClient BedrockManagementAPI
model *bedrock_support.BedrockModel
client *bedrockruntime.BedrockRuntime
model string
temperature float32
topP float32
maxTokens int
models []bedrock_support.BedrockModel
topP float32
maxTokens int
}
// AmazonCompletion BedRock support region list US East (N. Virginia),US West (Oregon),Asia Pacific (Singapore),Asia Pacific (Tokyo),Europe (Frankfurt)
// Amazon BedRock support region list US East (N. Virginia),US West (Oregon),Asia Pacific (Singapore),Asia Pacific (Tokyo),Europe (Frankfurt)
// https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html#bedrock-regions
const BEDROCK_DEFAULT_REGION = "us-east-1" // default use us-east-1 region
@@ -43,9 +34,6 @@ 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{
@@ -54,294 +42,45 @@ 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,
}
var defaultModels = []bedrock_support.BedrockModel{
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"
)
{
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",
},
},
var BEDROCK_MODELS = []string{
ModelAnthropicClaudeV2,
ModelAnthropicClaudeV1,
ModelAnthropicClaudeInstantV1,
ModelA21J2UltraV1,
ModelA21J2JumboInstruct,
ModelAmazonTitanExpressV1,
}
// 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,
//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
}
}
// 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")
}
@@ -356,138 +95,23 @@ func GetRegionOrDefault(region string) string {
return BEDROCK_DEFAULT_REGION
}
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)
}
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)
}
// 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")
}
// 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
// Create a new AWS session
providerRegion := GetRegionOrDefault(config.GetProviderRegion())
sess, err := session.NewSession(&aws.Config{
Region: aws.String(providerRegion),
})
if err != nil {
return err
}
// 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
// Create a new BedrockRuntime client
a.client = bedrockruntime.New(sess)
a.model = GetModelOrDefault(config.GetModel())
a.temperature = config.GetTemperature()
a.topP = config.GetTopP()
a.maxTokens = config.GetMaxTokens()
@@ -495,84 +119,41 @@ func (a *AmazonBedRockClient) Configure(config IAIConfig) error {
return nil
}
// 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]
// 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
// 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)
}
// 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)
body, err := json.Marshal(request)
if err != nil {
return "", err
}
@@ -580,46 +161,66 @@ 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.Config.ModelName),
ModelId: aws.String(a.model),
ContentType: aws.String("application/json"),
Accept: aws.String("application/json"),
}
// 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)
}
resp, err := a.client.InvokeModelWithContext(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
}
// Parse the response
return a.model.Response.ParseResponse(resp.Body)
// 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)
}
}
// GetName returns the name of the AmazonBedRockClient.
func (a *AmazonBedRockClient) GetName() string {
return amazonbedrockAIClientName

View File

@@ -1,103 +0,0 @@
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)
}

View File

@@ -1,262 +0,0 @@
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")
})
}
}

View File

@@ -14,10 +14,10 @@ const azureAIClientName = "azureopenai"
type AzureAIClient struct {
nopCloser
client *openai.Client
model string
temperature float32
// organizationId string
client *openai.Client
model string
temperature float32
organizationId string
}
func (c *AzureAIClient) Configure(config IAIConfig) error {

View File

@@ -1,18 +0,0 @@
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)
}

View File

@@ -1,140 +0,0 @@
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
}

View File

@@ -1,182 +0,0 @@
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))
}

View File

@@ -1,14 +0,0 @@
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
}

View File

@@ -1,59 +0,0 @@
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
}

View File

@@ -1,155 +0,0 @@
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
}

View File

@@ -1,65 +0,0 @@
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)
}

View File

@@ -1,147 +0,0 @@
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
}

View File

@@ -1,87 +0,0 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
}

View File

@@ -80,10 +80,10 @@ func (c *GoogleGenAIClient) GetCompletion(ctx context.Context, prompt string) (s
if !r.Blocked {
continue
}
return "", fmt.Errorf("completion blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
return "", fmt.Errorf("complection blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
}
}
return "", errors.New("no completion returned; unknown reason")
return "", errors.New("no complection returned; unknown reason")
}
// Format output.

View File

@@ -61,22 +61,10 @@ var VERTEXAI_SUPPORTED_REGION = []string{
}
const (
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
ModelGeminiProV1 = "gemini-1.0-pro-001"
)
var VERTEXAI_MODELS = []string{
ModelGeminiProV2_5,
ModelGeminiFlashV2_5,
ModelGeminiFlashV2,
ModelGeminiFlashLiteV2,
ModelGeminiProV1_5,
ModelGeminiFlashV1_5,
ModelGeminiProV1,
}
@@ -135,7 +123,7 @@ func (g *GoogleVertexAIClient) GetCompletion(ctx context.Context, prompt string)
model := g.client.GenerativeModel(g.model)
model.SetTemperature(g.temperature)
model.SetTopP(g.topP)
model.SetTopK(g.topK)
model.SetTopK(float32(g.topK))
model.SetMaxOutputTokens(int32(g.maxTokens))
// Google AI SDK is capable of different inputs than just text, for now set explicit text prompt type.
@@ -151,10 +139,10 @@ func (g *GoogleVertexAIClient) GetCompletion(ctx context.Context, prompt string)
if !r.Blocked {
continue
}
return "", fmt.Errorf("completion blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
return "", fmt.Errorf("complection blocked due to %v with probability %v", r.Category.String(), r.Probability.String())
}
}
return "", errors.New("no completion returned; unknown reason")
return "", errors.New("no complection returned; unknown reason")
}
// Format output.

View File

@@ -1,102 +0,0 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
}

View File

@@ -32,9 +32,7 @@ var (
&HuggingfaceClient{},
&GoogleVertexAIClient{},
&OCIGenAIClient{},
&CustomRestClient{},
&IBMWatsonxAIClient{},
&GroqClient{},
}
Backends = []string{
openAIClientName,
@@ -49,9 +47,7 @@ var (
huggingfaceAIClientName,
googleVertexAIClientName,
ociClientName,
CustomRestClientName,
ibmWatsonxAIClientName,
groqAIClientName,
}
)
@@ -185,7 +181,7 @@ func (p *AIProvider) GetCustomHeaders() []http.Header {
return p.CustomHeaders
}
var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "googlevertexai", "oci", "customrest"}
var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "googlevertexai", "oci"}
func NeedPassword(backend string) bool {
for _, b := range passwordlessProviders {

View File

@@ -16,32 +16,21 @@ 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"
"reflect"
"strings"
)
const ociClientName = "oci"
type ociModelVendor string
const (
vendorCohere = "cohere"
vendorMeta = "meta"
)
type OCIGenAIClient struct {
nopCloser
client *generativeaiinference.GenerativeAiInferenceClient
model *generativeai.Model
modelID string
model string
compartmentId string
temperature float32
topP float32
topK int32
maxTokens int
}
@@ -51,10 +40,9 @@ func (c *OCIGenAIClient) GetName() string {
func (c *OCIGenAIClient) Configure(config IAIConfig) error {
config.GetEndpointName()
c.modelID = config.GetModel()
c.model = 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()
@@ -63,123 +51,47 @@ 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) {
request := c.newChatRequest(prompt)
response, err := c.client.Chat(ctx, request)
generateTextRequest := c.newGenerateTextRequest(prompt)
generateTextResponse, err := c.client.GenerateText(ctx, generateTextRequest)
if err != nil {
return "", err
}
if err != nil {
return "", err
}
return extractGeneratedText(response.ChatResponse)
return extractGeneratedText(generateTextResponse.InferenceResponse)
}
func (c *OCIGenAIClient) newChatRequest(prompt string) generativeaiinference.ChatRequest {
return generativeaiinference.ChatRequest{
ChatDetails: generativeaiinference.ChatDetails{
func (c *OCIGenAIClient) newGenerateTextRequest(prompt string) generativeaiinference.GenerateTextRequest {
temperatureF64 := float64(c.temperature)
topPF64 := float64(c.topP)
return generativeaiinference.GenerateTextRequest{
GenerateTextDetails: generativeaiinference.GenerateTextDetails{
CompartmentId: &c.compartmentId,
ServingMode: c.getServingMode(),
ChatRequest: c.getChatModelRequest(prompt),
ServingMode: generativeaiinference.OnDemandServingMode{
ModelId: &c.model,
},
InferenceRequest: generativeaiinference.CohereLlmInferenceRequest{
Prompt: &prompt,
MaxTokens: &c.maxTokens,
Temperature: &temperatureF64,
TopP: &topPF64,
},
},
}
}
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,
}
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 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,
sb := strings.Builder{}
for _, text := range response.GeneratedTexts {
if text.Text != nil {
sb.WriteString(*text.Text)
}
}
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)
return sb.String(), nil
}

View File

@@ -27,11 +27,11 @@ const openAIClientName = "openai"
type OpenAIClient struct {
nopCloser
client *openai.Client
model string
temperature float32
topP float32
// organizationId string
client *openai.Client
model string
temperature float32
topP float32
organizationId string
}
const (
@@ -95,7 +95,7 @@ func (c *OpenAIClient) GetCompletion(ctx context.Context, prompt string) (string
},
},
Temperature: c.temperature,
MaxCompletionTokens: maxToken,
MaxTokens: maxToken,
PresencePenalty: presencePenalty,
FrequencyPenalty: frequencyPenalty,
TopP: c.topP,

View File

@@ -6,6 +6,8 @@ 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.
@@ -56,12 +58,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,

View File

@@ -16,13 +16,11 @@ package analysis
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"sync"
"time"
"github.com/fatih/color"
openapi_v2 "github.com/google/gnostic/openapiv2"
@@ -35,7 +33,6 @@ 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 {
@@ -53,8 +50,6 @@ type Analysis struct {
MaxConcurrency int
AnalysisAIProvider string // The name of the AI Provider used for this analysis
WithDoc bool
WithStats bool
Stats []common.AnalysisStats
}
type (
@@ -87,40 +82,23 @@ func NewAnalysis(
withDoc bool,
interactiveMode bool,
httpHeaders []string,
withStats bool,
) (*Analysis, error) {
// 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{
@@ -134,23 +112,6 @@ func NewAnalysis(
Explain: explain,
MaxConcurrency: maxConcurrency,
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.
@@ -158,9 +119,6 @@ func NewAnalysis(
}
var configAI ai.AIConfiguration
if verbose {
fmt.Println("Debug: Checking AI configuration.")
}
if err := viper.UnmarshalKey("ai", &configAI); err != nil {
return nil, err
}
@@ -173,16 +131,10 @@ 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
@@ -197,23 +149,12 @@ 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
@@ -228,15 +169,6 @@ 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())
@@ -246,18 +178,6 @@ 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{}{}
@@ -270,9 +190,6 @@ func (a *Analysis) RunCustomAnalysis() {
mutex.Unlock()
return
}
if verbose {
fmt.Printf("Debug: %s launched.\n", cAnalyzer.Name)
}
result, err := canClient.Run()
if result.Kind == "" {
@@ -285,16 +202,10 @@ 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)
@@ -304,7 +215,6 @@ func (a *Analysis) RunCustomAnalysis() {
func (a *Analysis) RunAnalysis() {
activeFilters := viper.GetStringSlice("active_filters")
verbose := viper.GetBool("verbose")
coreAnalyzerMap, analyzerMap := analyzer.GetAnalyzerMap()
@@ -313,13 +223,7 @@ 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))
}
@@ -334,27 +238,27 @@ func (a *Analysis) RunAnalysis() {
OpenapiSchema: openapiSchema,
}
// 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)
semaphore := make(chan struct{}, a.MaxConcurrency)
var wg sync.WaitGroup
var mutex sync.Mutex
// if there are no filters selected and no active_filters then run coreAnalyzer
if len(a.Filters) == 0 && len(activeFilters) == 0 {
if verbose {
fmt.Println("Debug: No filters selected and no active filters found, run all core analyzers.")
}
for name, analyzer := range coreAnalyzerMap {
for _, analyzer := range coreAnalyzerMap {
wg.Add(1)
semaphore <- struct{}{}
go a.executeAnalyzer(analyzer, name, analyzerConfig, semaphore, &wg, &mutex)
go func(analyzer common.IAnalyzer, wg *sync.WaitGroup, semaphore chan struct{}) {
defer wg.Done()
results, err := analyzer.Analyze(analyzerConfig)
if err != nil {
mutex.Lock()
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", reflect.TypeOf(analyzer).Name(), err))
mutex.Unlock()
}
mutex.Lock()
a.Results = append(a.Results, results...)
mutex.Unlock()
<-semaphore
}(analyzer, &wg, semaphore)
}
wg.Wait()
@@ -362,14 +266,23 @@ 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{}{}
wg.Add(1)
go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex)
go func(analyzer common.IAnalyzer, filter string) {
defer wg.Done()
results, err := analyzer.Analyze(analyzerConfig)
if err != nil {
mutex.Lock()
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
mutex.Unlock()
}
mutex.Lock()
a.Results = append(a.Results, results...)
mutex.Unlock()
<-semaphore
}(analyzer, filter)
} else {
a.Errors = append(a.Errors, fmt.Sprintf("\"%s\" filter does not exist. Please run k8sgpt filters list.", filter))
}
@@ -379,81 +292,33 @@ 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{}{}
wg.Add(1)
go a.executeAnalyzer(analyzer, filter, analyzerConfig, semaphore, &wg, &mutex)
go func(analyzer common.IAnalyzer, filter string) {
defer wg.Done()
results, err := analyzer.Analyze(analyzerConfig)
if err != nil {
mutex.Lock()
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
mutex.Unlock()
}
mutex.Lock()
a.Results = append(a.Results, results...)
mutex.Unlock()
<-semaphore
}(analyzer, filter)
}
}
wg.Wait()
}
func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, analyzerConfig common.Analyzer, semaphore chan struct{}, wg *sync.WaitGroup, mutex *sync.Mutex) {
defer wg.Done()
var startTime time.Time
var elapsedTime time.Duration
// Start the timer
if a.WithStats {
startTime = time.Now()
}
// Run the analyzer
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)
}
stat := common.AnalysisStats{
Analyzer: filter,
DurationTime: elapsedTime,
}
mutex.Lock()
defer mutex.Unlock()
if err != nil {
if a.WithStats {
a.Stats = append(a.Stats, stat)
}
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
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
}
func (a *Analysis) GetAIResults(output string, anonymize bool) error {
if len(a.Results) == 0 {
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)))
@@ -462,10 +327,6 @@ 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 {
@@ -536,24 +397,6 @@ 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

View File

@@ -17,17 +17,13 @@ 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"
@@ -35,15 +31,9 @@ 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"
)
// helper function: get type name of an analyzer
func getTypeName(i interface{}) string {
return reflect.TypeOf(i).Name()
}
// helper function: run analysis with filter
// sub-function
func analysis_RunAnalysisFilterTester(t *testing.T, filterFlag string) []common.Result {
clientset := fake.NewSimpleClientset(
&v1.Pod{
@@ -414,252 +404,3 @@ 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)
}
}

View File

@@ -55,18 +55,6 @@ func (a *Analysis) jsonOutput() ([]byte, error) {
return output, nil
}
func (a *Analysis) PrintStats() []byte {
var output strings.Builder
output.WriteString(color.YellowString("The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.\n"))
for _, stat := range a.Stats {
output.WriteString(fmt.Sprintf("- Analyzer %s took %s \n", color.YellowString(stat.Analyzer), stat.DurationTime))
}
return []byte(output.String())
}
func (a *Analysis) textOutput() ([]byte, error) {
var output strings.Builder

View File

@@ -39,31 +39,20 @@ 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) {

View File

@@ -1,53 +0,0 @@
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
}

View File

@@ -1,107 +0,0 @@
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))
}
}

View File

@@ -1,161 +0,0 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"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

@@ -1,182 +0,0 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"context"
"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

@@ -1,148 +0,0 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"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

@@ -1,179 +0,0 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"context"
"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

@@ -1,82 +0,0 @@
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 ""
}

View File

@@ -1,78 +0,0 @@
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)
}
}

View File

@@ -1,125 +0,0 @@
/*
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
}

View File

@@ -1,149 +0,0 @@
/*
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))
})
}
}

View File

@@ -22,274 +22,179 @@ 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) {
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{
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{
ObjectMeta: metav1.ObjectMeta{
Name: "suspended-job",
Namespace: "default",
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.CronJobSpec{
Schedule: "*/5 * * * *",
Suspend: boolPtr(true),
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "example-container",
Image: "nginx",
},
},
RestartPolicy: v1.RestartPolicyOnFailure,
},
},
},
},
),
},
},
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",
}
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))
cjAnalyzer := CronJobAnalyzer{}
results, err := cjAnalyzer.Analyze(config)
require.NoError(t, err)
// Sort results by name for consistent comparison
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
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)
}
})
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)
}
}
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
},
},
)
func TestCronJobAnalyzerLabelSelectorFiltering(t *testing.T) {
suspend := new(bool)
*suspend = true
invalidStartingDeadline := new(int64)
*invalidStartingDeadline = -7
validStartingDeadline := new(int64)
*validStartingDeadline = 7
// Test with label selector
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientSet,
Client: fake.NewSimpleClientset(
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "CJ1",
Namespace: "default",
Labels: map[string]string{
"app": "cronjob",
},
},
},
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "CJ2",
Namespace: "default",
},
},
),
},
Context: context.Background(),
Namespace: "default",
LabelSelector: "app=test",
LabelSelector: "app=cronjob",
}
analyzer := CronJobAnalyzer{}
results, err := analyzer.Analyze(config)
cjAnalyzer := CronJobAnalyzer{}
results, err := cjAnalyzer.Analyze(config)
require.NoError(t, err)
require.Equal(t, 1, len(results))
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)
}
})
}
require.Equal(t, "default/CJ1", results[0].Name)
}

View File

@@ -54,41 +54,22 @@ 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.ReadyReplicas {
if deployment.Status.Replicas > *deployment.Spec.Replicas {
doc := apiDoc.GetApiDocV2("spec.replicas")
if *deployment.Spec.Replicas != deployment.Status.Replicas {
doc := apiDoc.GetApiDocV2("spec.replicas")
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),
},
}})
}
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),
},
}})
}
if len(failures) > 0 {
preAnalysis[fmt.Sprintf("%s/%s", deployment.Namespace, deployment.Name)] = common.PreAnalysis{

View File

@@ -1,137 +0,0 @@
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)
}
}
})
}
}

View File

@@ -20,7 +20,6 @@ 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"
@@ -35,7 +34,7 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
Kind: kind,
ApiVersion: schema.GroupVersion{
Group: "autoscaling",
Version: "v2",
Version: "v1",
},
OpenapiSchema: a.OpenapiSchema,
}
@@ -54,25 +53,15 @@ 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 {
// 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{},
})
}
if condition.Status != "True" {
failures = append(failures, common.Failure{
Text: condition.Message,
Sensitive: []common.Sensitive{},
})
}
}

View File

@@ -566,6 +566,7 @@ func TestHPAAnalyzerLabelSelectorFiltering(t *testing.T) {
assert.Equal(t, len(analysisResults), 1)
}
func TestHPAAnalyzerStatusFieldAbleToScale(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv2.HorizontalPodAutoscaler{
@@ -583,8 +584,8 @@ func TestHPAAnalyzerStatusFieldAbleToScale(t *testing.T) {
Status: autoscalingv2.HorizontalPodAutoscalerStatus{
Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
{
Type: "AbleToScale",
Status: "False",
Type: "AbleToScale",
Status: "False",
Message: "test reason",
},
},
@@ -606,6 +607,7 @@ func TestHPAAnalyzerStatusFieldAbleToScale(t *testing.T) {
}
func TestHPAAnalyzerStatusFieldScalingActive(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv2.HorizontalPodAutoscaler{
@@ -623,8 +625,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",
},
},
@@ -646,6 +648,8 @@ func TestHPAAnalyzerStatusFieldScalingActive(t *testing.T) {
}
func TestHPAAnalyzerStatusFieldScalingLimited(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv2.HorizontalPodAutoscaler{
@@ -663,8 +667,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",
},
},
@@ -686,6 +690,7 @@ func TestHPAAnalyzerStatusFieldScalingLimited(t *testing.T) {
}
func TestHPAAnalyzerStatusField(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv2.HorizontalPodAutoscaler{
@@ -703,18 +708,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",
},
},
@@ -734,88 +739,4 @@ 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)
}
}
}

View File

@@ -15,243 +15,226 @@ 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) {
// Create test cases
testCases := []struct {
name string
ingress *networkingv1.Ingress
expectedIssues []string
}{
{
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,
},
},
},
},
},
},
},
},
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",
},
},
},
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: "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,
},
},
},
},
},
},
},
},
{
Path: "/test1",
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
// This service is in the test namespace
// Hence, it won't be discovered.
Name: "Service2",
},
},
},
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: "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,
},
},
},
},
},
},
},
},
{
Path: "/test2",
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
// This service doesn't exist.
Name: "Service3",
},
},
},
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.",
},
},
}
igRule.IngressRuleValue.HTTP = &httpRule
// 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()
// 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,
config := common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&networkingv1.Ingress{
// Doesn't specify an ingress class.
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress1",
Namespace: "default",
},
},
Context: ctx,
Namespace: tc.ingress.Namespace,
}
&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",
},
// Create the analyzer and run analysis
analyzer := IngressAnalyzer{}
results, err := analyzer.Analyze(config)
assert.NoError(t, err)
// 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",
}
// 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))
igAnalyzer := IngressAnalyzer{}
results, err := igAnalyzer.Analyze(config)
require.NoError(t, err)
// 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)
}
})
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
expectations := []struct {
name string
failuresCount int
}{
{
name: "default/Ingress1",
failuresCount: 1,
},
{
name: "default/Ingress2",
failuresCount: 1,
},
{
name: "default/Ingress5",
failuresCount: 4,
},
}
require.Equal(t, len(expectations), len(results))
for i, result := range results {
require.Equal(t, expectations[i].name, result.Name)
require.Equal(t, expectations[i].failuresCount, len(result.Error))
}
}
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
},
},
)
func TestIngressAnalyzerLabelSelectorFiltering(t *testing.T) {
validIgClassName := new(string)
*validIgClassName = "valid-ingress-class"
// Test with label selector
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientSet,
Client: fake.NewSimpleClientset(
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress1",
Namespace: "default",
Labels: map[string]string{
"app": "ingress",
},
},
},
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress2",
Namespace: "default",
},
},
),
},
Context: context.Background(),
Namespace: "default",
LabelSelector: "app=test",
LabelSelector: "app=ingress",
}
analyzer := IngressAnalyzer{}
results, err := analyzer.Analyze(config)
igAnalyzer := IngressAnalyzer{}
results, err := igAnalyzer.Analyze(config)
require.NoError(t, err)
require.Equal(t, 1, len(results))
require.Equal(t, "default/ingress-with-label", results[0].Name)
}
require.Equal(t, "default/Ingress1", results[0].Name)
// Helper functions
func strPtr(s string) *string {
return &s
}
func pathTypePtr(p networkingv1.PathType) *networkingv1.PathType {
return &p
}

View File

@@ -1,75 +0,0 @@
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)
}
}

View File

@@ -1,72 +0,0 @@
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
}

View File

@@ -1,107 +0,0 @@
/*
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
}

View File

@@ -1,215 +0,0 @@
/*
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)
}

View File

@@ -46,17 +46,15 @@ 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 {
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
if nodeCondition.Status == v1.ConditionTrue {
break
}
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
// k3s `EtcdIsVoter`` should not be reported as an error
case v1.NodeConditionType("EtcdIsVoter"):
break
default:
// 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) {
if nodeCondition.Status != v1.ConditionFalse {
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
}
}
@@ -101,17 +99,3 @@ 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
}
}

View File

@@ -1,46 +0,0 @@
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
}

View File

@@ -1,70 +0,0 @@
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])
}
}

View File

@@ -123,20 +123,6 @@ 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" {

View File

@@ -343,57 +343,6 @@ 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{}

View File

@@ -1,201 +0,0 @@
/*
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
}

View File

@@ -1,181 +0,0 @@
/*
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))
})
}
}

View File

@@ -24,232 +24,145 @@ 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) {
tests := []struct {
name string
config common.Analyzer
expectations []struct {
name string
failuresCount int
}
}{
{
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: "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{
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{
{
NotReadyAddresses: []v1.EndpointAddress{
{
TargetRef: &v1.ObjectReference{
Kind: "Pod",
Name: "test-pod",
},
},
TargetRef: &v1.ObjectReference{
Kind: "test-reference",
Name: "reference1",
},
},
{
TargetRef: &v1.ObjectReference{
Kind: "test-reference",
Name: "reference2",
},
},
},
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
Spec: v1.ServiceSpec{
Selector: map[string]string{
"app": "test",
{
// These not ready end points will contribute to failures.
NotReadyAddresses: []v1.EndpointAddress{
{
TargetRef: &v1.ObjectReference{
Kind: "test-reference",
Name: "reference3",
},
},
},
},
),
},
},
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/test-service",
failuresCount: 1, // One failure for not ready endpoints
&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
}{
{
name: "test/Endpoint1",
failuresCount: 1,
},
{
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
},
name: "test/Service1",
failuresCount: 2,
},
}
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))
require.Equal(t, len(expectations), len(results))
// 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)
}
})
for i, result := range results {
require.Equal(t, expectations[i].name, result.Name)
require.Equal(t, expectations[i].failuresCount, len(result.Error))
}
}

View File

@@ -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."

View File

@@ -1,216 +0,0 @@
/*
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
}

View File

@@ -1,254 +0,0 @@
/*
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)
}
})
}
}

View File

@@ -1,55 +0,0 @@
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
}

View File

@@ -1,78 +0,0 @@
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)
}
}

View File

@@ -1,10 +0,0 @@
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
View File

@@ -14,7 +14,6 @@ var (
&FileBasedCache{},
&GCSCache{},
&S3Cache{},
&InterplexCache{},
}
)
@@ -55,21 +54,15 @@ 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))
}
@@ -90,19 +83,20 @@ func GetCacheConfiguration() (ICache, error) {
}
var cache ICache
switch {
case cacheInfo.CurrentCacheType == "gcs":
case cacheInfo.GCS != GCSCacheConfiguration{}:
cache = &GCSCache{}
case cacheInfo.CurrentCacheType == "azure":
case cacheInfo.Azure != AzureCacheConfiguration{}:
cache = &AzureCache{}
case cacheInfo.CurrentCacheType == "s3":
case cacheInfo.S3 != S3CacheConfiguration{}:
cache = &S3Cache{}
case cacheInfo.CurrentCacheType == "interplex":
cache = &InterplexCache{}
default:
cache = &FileBasedCache{}
}
err_config := cache.Configure(cacheInfo)
return cache, err_config
}

View File

@@ -1,60 +0,0 @@
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)
}

View File

@@ -1,77 +0,0 @@
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")
}

View File

@@ -1,131 +0,0 @@
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
}

View File

@@ -1,90 +0,0 @@
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
View File

@@ -3,9 +3,8 @@ package cache
import (
"bytes"
"crypto/tls"
"errors"
"log"
"net/http"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
@@ -28,19 +27,16 @@ type S3CacheConfiguration struct {
func (s *S3Cache) Configure(cacheInfo CacheProvider) error {
if cacheInfo.S3.BucketName == "" {
return errors.New("bucket name not configured")
log.Fatal("Bucket name not configured")
}
s.bucketName = cacheInfo.S3.BucketName
sess, err := session.NewSessionWithOptions(session.Options{
sess := session.Must(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)
@@ -54,14 +50,10 @@ 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
View File

@@ -3,11 +3,9 @@ package cache
import "time"
type CacheProvider struct {
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"`
GCS GCSCacheConfiguration `mapstructucre:"gcs" yaml:"gcs,omitempty"`
Azure AzureCacheConfiguration `mapstructucre:"azure" yaml:"azure,omitempty"`
S3 S3CacheConfiguration `mapstructucre:"s3" yaml:"s3,omitempty"`
}
type CacheObjectDetails struct {

Some files were not shown because too many files have changed in this diff Show More