Compare commits

..

107 Commits

Author SHA1 Message Date
github-actions[bot]
8bbffed643 chore(main): release 0.4.24 (#1560)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-24 18:59:36 +01:00
Umesh Kaul
53345895de feat: update helm charts with mcp support and fix Google ADA issue (#1568)
* migrated to more actively maintained mcp golang lib and added AI explain support for mcp mode

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

* added a makefile option to create local docker image for testing

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

* fixed linter errors and made anonymize as an arg

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

* added mcp support for helm chart and fixed google adk support issue

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

---------

Signed-off-by: Umesh Kaul <umeshkaul@gmail.com>
Co-authored-by: Alex Jones <1235925+AlexsJones@users.noreply.github.com>
2025-08-18 19:33:12 +01:00
Alex Jones
7e332761d8 feat: reintroduced inference code (#1548)
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2025-08-18 19:32:11 +01:00
renovate[bot]
0239b2fe6e chore(deps): update docker/login-action digest to 184bdaa (#1559)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-17 20:17:52 +01:00
renovate[bot]
c5c9135900 chore(deps): update amannn/action-semantic-pull-request action to v6 (#1565)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-16 12:06:30 +01:00
renovate[bot]
5e86f4925c chore(deps): update goreleaser/goreleaser-action digest to e435ccd (#1569)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-15 14:07:28 +01:00
Bruno Andrade
0cf4cae07e feat: add ClusterServiceVersion, Subscription, InstallPlan, OperatorGroup, and CatalogSource analyzers (#1564)
Signed-off-by: Bruno Andrade <bruno.balint@gmail.com>
2025-08-13 17:39:12 +01:00
renovate[bot]
e385e77da9 chore(deps): update actions/checkout action to v5 (#1562)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-12 16:56:30 +01:00
Umesh Kaul
c47ae595fb fix: migrated to more actively maintained mcp golang lib and added AI explain (#1557)
* migrated to more actively maintained mcp golang lib and added AI explain support for mcp mode

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

* added a makefile option to create local docker image for testing

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

* fixed linter errors and made anonymize as an arg

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

---------

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

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

* added name and version for mcp server

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

* added tests

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

* chore: fixed linter

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

* fixed linter issues in server_test.go

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

---------

Signed-off-by: Umesh Kaul <umeshkaul@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
Co-authored-by: AlexsJones <alexsimonjones@gmail.com>
2025-07-18 15:14:54 +01:00
koichi
1819e6f410 feat: add APAC region Claude models support for Amazon Bedrock (#1543)
Signed-off-by: Koichi Shimada <jumpe1programming@gmail.com>
2025-07-14 11:24:03 +01:00
github-actions[bot]
392c79d0be chore(main): release 0.4.21 (#1536)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-27 11:55:09 +01:00
HarelMil
00c07999e2 feat: add latest and legacy stable models (#1539)
Signed-off-by: HarelMil <HarelMil@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-06-27 08:34:23 +01:00
Alex Jones
8002d94345 feat: support for claude4 && model names listed (#1540)
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2025-06-27 08:24:38 +01:00
renovate[bot]
0c917fc601 chore(deps): update docker/setup-buildx-action digest to e468171 (#1527)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 14:47:12 +01:00
renovate[bot]
08f2855a4d fix(deps): update module gopkg.in/yaml.v2 to v3 (#1511)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-20 17:20:36 +01:00
github-actions[bot]
57d32720dc chore(main): release 0.4.20 (#1533)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-20 16:56:13 +01:00
Alex Jones
0f700f0cd3 chore: model name (#1535)
* feat: added cache purge

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

* feat: improved AWS creds errors

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

* chore: removed model name

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

* chore: updated tests

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

---------

Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2025-06-20 16:30:50 +01:00
Alex Jones
74fbde0053 feat: added cache purge (#1532)
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2025-06-20 16:00:08 +01:00
github-actions[bot]
ff619481cc chore(main): release 0.4.19 (#1531)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-20 13:54:13 +01:00
Alex Jones
5636515db9 feat: fixed haiku (#1530)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-06-20 13:49:24 +01:00
github-actions[bot]
47f09ac686 chore(main): release 0.4.18 (#1510)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-06-20 13:28:54 +01:00
Alex Jones
be4fb1cc03 chore: model access (#1529)
* chore: improve the node analyzer reporting false positives

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

* feat: improving the bedrock model access message

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

* feat: improving the bedrock model access message

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

* feat: improving the bedrock model access message

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

* chore: repairing tests

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

---------

Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
2025-06-20 13:27:49 +01:00
renovate[bot]
5947876e49 chore(deps): update softprops/action-gh-release digest to 72f2c25 (#1526)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 06:17:02 +01:00
renovate[bot]
7d4cb26713 fix(deps): update k8s.io/utils digest to 4c0f3b2 (#1523)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 19:04:24 +01:00
renovate[bot]
6b9f346bf6 chore(deps): update softprops/action-gh-release digest to d5382d3 (#1525)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 18:50:49 +01:00
renovate[bot]
42654e7f55 chore(deps): update codecov/codecov-action digest to 18283e0 (#1513)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-27 18:56:55 +01:00
renovate[bot]
7dfe8bef0f chore(deps): update docker/build-push-action digest to 2634353 (#1517)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-27 18:43:24 +01:00
renovate[bot]
dfcc5dc5a1 chore(deps): update docker/build-push-action digest to 1dc7386 (#1512)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-20 11:21:03 +01:00
renovate[bot]
d7cb19ad29 fix(deps): update module gopkg.in/yaml.v2 to v3 (#1509)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-15 09:08:24 +01:00
github-actions[bot]
306b3c9997 chore(main): release 0.4.17 (#1499)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-14 20:57:55 +01:00
renovate[bot]
1e57b7774c chore(deps): update golangci/golangci-lint-action action to v8 (#1490)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-14 20:45:57 +01:00
renovate[bot]
d308c511fb fix(deps): update module gopkg.in/yaml.v2 to v3 (#1500)
* fix(deps): update module gopkg.in/yaml.v2 to v3

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore: resolved conflict in deps

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

---------

Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-05-14 20:38:36 +01:00
Alex Jones
4faf77d91a chore: golangci lint (#1508)
* feat: added token for goreleaser

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

* feat: updated the bedrock supported regions

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

* chore: updating golangci_lint

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

---------

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-05-14 17:04:49 +01:00
rkarthikr
b2241c03c9 feat: adding fixes for Messages API issue 1391 (#1504)
Signed-off-by: rkarthikr <38294804+rkarthikr@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-05-14 14:33:54 +01:00
Kay Yan
0b7ddf5e3b feat: new job analyzer (#1506)
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
2025-05-14 09:22:05 +01:00
renovate[bot]
d0f03641ae fix(deps): update module gopkg.in/yaml.v2 to v3 (#1454)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-09 15:30:11 +01:00
renovate[bot]
e76bdb0c23 chore(deps): update actions/setup-go digest to d35c59a (#1495)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-09 13:17:19 +01:00
typeid
cae94e7b6d fix: panic in k8sgpt auth update (#1497)
Signed-off-by: Claudio Busse <cbusse@redhat.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-05-09 13:02:37 +01:00
typeid
7e375a30be fix: align documentation to reflect default analyzers properly (#1498)
Signed-off-by: Claudio Busse <cbusse@redhat.com>
2025-05-09 12:58:29 +01:00
github-actions[bot]
34ff645fa0 chore(main): release 0.4.16 (#1478)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-06 19:24:25 +01:00
Naveen Thangaraj
61b60d5768 feat: enhancement of deployment analyzer (#1406)
* Updated the deployment analyzer

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

* Enhanced the deployment analyzer

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

---------

Signed-off-by: naveenthangaraj03 <tnaveen3402@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-05-06 16:30:16 +01:00
renovate[bot]
6a81d2c140 fix(deps): update k8s.io/utils digest to 0f33e8f (#1484)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 11:30:48 +01:00
rkarthikr
21bc76e5b7 feat: add support for Amazon Bedrock Inference Profiles (#1492)
Signed-off-by: rkarthikr <38294804+rkarthikr@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-05-06 11:18:40 +01:00
renovate[bot]
d5341f3c00 chore(deps): update golangci/golangci-lint-action digest to 9fae48a (#1489)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 09:23:12 +01:00
Alex Jones
752a16c407 feat: supported regions govcloud (#1483)
* feat: added token for goreleaser

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

* feat: updated the bedrock supported regions

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

---------

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-05-01 09:01:25 +01:00
renovate[bot]
81da402d46 chore(deps): update docker/build-push-action digest to 14487ce (#1472)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 13:58:23 +01:00
github-actions[bot]
f2f25edef7 chore(main): release 0.4.15 (#1477)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-29 12:57:30 +01:00
Alex Jones
85935a46d8 feat: added token for goreleaser (#1476)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-29 12:49:44 +01:00
github-actions[bot]
a56e663169 chore(main): release 0.4.14 (#1470)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-29 09:26:48 +01:00
Alex Jones
e41ffd80d0 feat: add MCP support (#1471)
* feat: first mcp impl

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

* chore: update

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

* chore: wip

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

* chore: switcheed to stdio transport

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

* chore: readme

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

* feat: fix the linter 🤖

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

* feat: fix the linter 🤖

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

* feat(mcp): implement MCP server and handler

- Implement MCP server and handler
- Add MCP server to serve
- Add MCP handler to handle MCP requests
- Add MCP server to serve
- Add MCP handler to handle MCP requests

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

* feat: consolidating code duplication

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

* feat: added http sse support

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

* chore: fixed broken tests

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

* chore: updated and fixed linter

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

* chore: updated and fixed linter

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

* chore: updated the linter issues

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

---------

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-29 09:22:44 +01:00
ju187
f603948935 feat: using modelName will calling completion (#1469)
* using modelName will calling completion

Signed-off-by: Tony Chen <tony_chen@discovery.com>

* sign

Signed-off-by: Tony Chen <tony_chen@discovery.com>

---------

Signed-off-by: Tony Chen <tony_chen@discovery.com>
2025-04-24 09:15:17 +01:00
github-actions[bot]
67f5855695 chore(main): release 0.4.13 (#1465)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-22 11:28:20 +01:00
Antoine Deschênes
ebb0373f69 fix: reverse hpa ScalingLimited error condition (#1366)
* fix: reverse hpa ScalingLimited error condition

Signed-off-by: Antoine Deschênes <antoine.deschenes@linux.com>

* chore: removed break

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

---------

Signed-off-by: Antoine Deschênes <antoine.deschenes@linux.com>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-22 11:27:02 +01:00
Alex Jones
3b6ad06de1 feat: slack announce (#1466)
* chore: added slack integration on release

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

* chore: patched two go dep security warnings

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

---------

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-22 10:49:52 +01:00
renovate[bot]
443469960a chore(deps): update softprops/action-gh-release digest to da05d55 (#1464)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-19 20:27:30 +01:00
github-actions[bot]
17863c24d5 chore(main): release 0.4.12 (#1460)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-17 20:57:10 +01:00
renovate[bot]
e588fc316d fix(deps): update module golang.org/x/net to v0.38.0 [security] (#1462)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 20:50:44 +01:00
Alex Jones
a128906136 feat: new analyzers (#1459)
* chore: rebased
chore: removed trivy

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

* chore: updated deps

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

* fix: missing error

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

* fix: missing error

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

* feat: switching old sonnet to message API

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

* feat: added three new analyzers

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

* chore(main): release 0.4.2 (#1400)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* docs: remove extra dollar sign in README.md (#1410)

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

* test: add tests for `k8sgpt/pkg/analyzer/events.go` (#913)

* test: add tests for events_test.go

Signed-off-by: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com>

* feat: fixed event tests

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

---------

Signed-off-by: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* docs: add table of contents and cleanup (#1413)

Signed-off-by: hadi2f244 <m.h.azaddel@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore: linter (#1414)

* chore: changing linter

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

* chore: changing linter

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

* chore: changing linter

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

---------

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

* chore(deps): pin golangci/golangci-lint-action action to 1481404 (#1415)

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>

* chore(deps): update goreleaser/goreleaser-action digest to 9c156ee (#1411)

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>

* fix: prometheus UTF8Validation (#1404)

Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* fix(deps): update module gopkg.in/yaml.v2 to v3 (#1363)

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>

* chore: added new AmazonBedrock model  (#1390)

* Update AI Bedrock region - Added mumbai region

Signed-off-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>

* Update amazonbedrock.go

Signed-off-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>

* Added new AI model to work for ap-south-1 region[that does not uses inference profile]

Signed-off-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>

---------

Signed-off-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore(main): release 0.4.3 (#1412)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore(deps): update module github.com/docker/docker to v28 (#1376)

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>

* chore: updating deps (#1422)

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

* chore(deps): update docker/setup-buildx-action digest to b5ca514 (#1371)

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>

* chore(main): release 0.4.4 (#1421)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore: fix workflows (#1423)

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

* chore(main): release 0.4.5 (#1424)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore: fixing docker build push action (#1426)

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

* chore: updated actor for login (#1430)

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

* chore(deps): pin docker/build-push-action action to 471d1dc (#1428)

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>

* chore(main): release 0.4.6 (#1427)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore: fixing build (#1431)

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

* chore(deps): update actions/upload-artifact digest to ea165f8 (#1425)

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>

* chore(main): release 0.4.7 (#1432)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore: removed krew release (#1434)

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

* chore(main): release 0.4.8 (#1435)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore: fixing (#1437)

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

* chore(deps): pin dependencies (#1440)

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>

* chore(main): release 0.4.9 (#1439)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* fix: pod analyzer catches errors when containers are in Terminated state (#1438)

Signed-off-by: Guoxun Wei <guwe@microsoft.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* feat: add a naive support of bedrock inference profile (#1446)

* feat: add a naive support of bedrock inference profile

Signed-off-by: Tony Chen <tony_chen@discovery.com>

* feat: improving the tests

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

---------

Signed-off-by: Tony Chen <tony_chen@discovery.com>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* fix(deps): update module gopkg.in/yaml.v2 to v3 (#1417)

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>

* fix(deps): update module helm.sh/helm/v3 to v3.17.3 [security] (#1448)

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>

* chore(main): release 0.4.10 (#1441)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* feat: call bedrock with inference profile (#1449)

* call bedrock with inference profile

Signed-off-by: Tony Chen <tony_chen@discovery.com>

* add validation and test

Signed-off-by: Tony Chen <tony_chen@discovery.com>

* update test

Signed-off-by: Tony Chen <tony_chen@discovery.com>

---------

Signed-off-by: Tony Chen <tony_chen@discovery.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* fix(deps): update module gopkg.in/yaml.v2 to v3 (#1447)

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>

* docs: fix the slack invite link (#1450)

Signed-off-by: Pengfei Ni <feiskyer@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* feat: add verbose flag to enable detailed output (#1420)

* feat: add verbose flag to enable detailed output

Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>

* test: add verbose output tests for analysis.go and root.go

Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>

---------

Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* fix(deps): update module gopkg.in/yaml.v2 to v3 (#1453)

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>

* feat: improved test coverage (#1455)

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

* fix: config ai provider in query (#1457)

Signed-off-by: Guoxun Wei <guwe@microsoft.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore(main): release 0.4.11 (#1451)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: AlexsJones <alexsimonjones@gmail.com>

* chore: fixed test

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

* chore: fixed test

---------

Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
Signed-off-by: Qian_Xiao <heyheyco@gmail.com>
Signed-off-by: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Signed-off-by: hadi2f244 <m.h.azaddel@gmail.com>
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
Signed-off-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>
Signed-off-by: Guoxun Wei <guwe@microsoft.com>
Signed-off-by: Tony Chen <tony_chen@discovery.com>
Signed-off-by: Pengfei Ni <feiskyer@gmail.com>
Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Qian_Xiao <heyheyco@gmail.com>
Co-authored-by: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com>
Co-authored-by: Hadi Azaddel <m.h.azaddel@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kay Yan <kay.yan@daocloud.io>
Co-authored-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>
Co-authored-by: gossion <guwe@microsoft.com>
Co-authored-by: ju187 <tony_chen@discovery.com>
Co-authored-by: Pengfei Ni <feiskyer@users.noreply.github.com>
Co-authored-by: Yicheng <36285652+zyc140345@users.noreply.github.com>
2025-04-15 13:43:38 +01:00
renovate[bot]
0553b984b7 chore(deps): update codecov/codecov-action digest to ad3126e (#1456)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 12:45:00 +01:00
github-actions[bot]
96d86d3eb0 chore(main): release 0.4.11 (#1451)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-15 11:24:06 +01:00
gossion
df17e3e728 fix: config ai provider in query (#1457)
Signed-off-by: Guoxun Wei <guwe@microsoft.com>
2025-04-15 11:11:40 +01:00
Alex Jones
80904e3063 feat: improved test coverage (#1455)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-14 14:15:26 +01:00
renovate[bot]
cf6f9289e1 fix(deps): update module gopkg.in/yaml.v2 to v3 (#1453)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 13:50:15 +01:00
Yicheng
a79224e2bf feat: add verbose flag to enable detailed output (#1420)
* feat: add verbose flag to enable detailed output

Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>

* test: add verbose output tests for analysis.go and root.go

Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>

---------

Signed-off-by: Yicheng <36285652+zyc140345@users.noreply.github.com>
2025-04-14 13:06:24 +01:00
Pengfei Ni
9ce33469d8 docs: fix the slack invite link (#1450)
Signed-off-by: Pengfei Ni <feiskyer@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-14 11:48:35 +01:00
renovate[bot]
969fe99b33 fix(deps): update module gopkg.in/yaml.v2 to v3 (#1447)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 09:53:59 +01:00
ju187
91d423b147 feat: call bedrock with inference profile (#1449)
* call bedrock with inference profile

Signed-off-by: Tony Chen <tony_chen@discovery.com>

* add validation and test

Signed-off-by: Tony Chen <tony_chen@discovery.com>

* update test

Signed-off-by: Tony Chen <tony_chen@discovery.com>

---------

Signed-off-by: Tony Chen <tony_chen@discovery.com>
2025-04-11 07:11:38 +01:00
github-actions[bot]
766b51cd3e chore(main): release 0.4.10 (#1441)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 21:45:59 +01:00
renovate[bot]
060a3b2a26 fix(deps): update module helm.sh/helm/v3 to v3.17.3 [security] (#1448)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-10 18:59:22 +01:00
renovate[bot]
ce4b3c2e7d fix(deps): update module gopkg.in/yaml.v2 to v3 (#1417)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-10 11:25:50 +01:00
ju187
78ffa5904a feat: add a naive support of bedrock inference profile (#1446)
* feat: add a naive support of bedrock inference profile

Signed-off-by: Tony Chen <tony_chen@discovery.com>

* feat: improving the tests

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

---------

Signed-off-by: Tony Chen <tony_chen@discovery.com>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-10 09:47:58 +01:00
gossion
dceda9a6a1 fix: pod analyzer catches errors when containers are in Terminated state (#1438)
Signed-off-by: Guoxun Wei <guwe@microsoft.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-08 10:49:53 +01:00
github-actions[bot]
e7783482ce chore(main): release 0.4.9 (#1439)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-08 09:17:34 +01:00
renovate[bot]
a5574ee49d chore(deps): pin dependencies (#1440)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-08 09:08:25 +01:00
Alex Jones
f68ff0efee chore: fixing (#1437)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-08 08:36:39 +01:00
github-actions[bot]
0c63044254 chore(main): release 0.4.8 (#1435)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-07 20:31:33 +01:00
Alex Jones
39ae2aa635 chore: removed krew release (#1434)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-07 20:28:36 +01:00
github-actions[bot]
05040da188 chore(main): release 0.4.7 (#1432)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-07 17:32:40 +01:00
renovate[bot]
9bffc7cff7 chore(deps): update actions/upload-artifact digest to ea165f8 (#1425)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-07 16:54:08 +01:00
Alex Jones
c5fe2c68d1 chore: fixing build (#1431)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-07 16:50:19 +01:00
github-actions[bot]
c1b267b818 chore(main): release 0.4.6 (#1427)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-07 15:22:49 +01:00
renovate[bot]
5086ccd659 chore(deps): pin docker/build-push-action action to 471d1dc (#1428)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-07 15:16:26 +01:00
Alex Jones
b6261026f8 chore: updated actor for login (#1430)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-07 15:07:49 +01:00
Alex Jones
1681aadac1 chore: fixing docker build push action (#1426)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-07 07:37:25 +01:00
github-actions[bot]
9874cef8bf chore(main): release 0.4.5 (#1424)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-07 06:51:58 +01:00
Alex Jones
3dbc9e1a20 chore: fix workflows (#1423)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-07 06:50:46 +01:00
github-actions[bot]
e0d66f43f7 chore(main): release 0.4.4 (#1421)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-06 20:41:54 +01:00
renovate[bot]
d4de5d9e3f chore(deps): update docker/setup-buildx-action digest to b5ca514 (#1371)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-06 20:34:35 +01:00
Alex Jones
5b7fb7e619 chore: updating deps (#1422)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-06 19:30:25 +01:00
renovate[bot]
68ddac0089 chore(deps): update module github.com/docker/docker to v28 (#1376)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-06 16:47:40 +01:00
github-actions[bot]
d1a29e4001 chore(main): release 0.4.3 (#1412)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-04 11:49:09 +01:00
Sakshi Singh
ad2c90a129 chore: added new AmazonBedrock model (#1390)
* Update AI Bedrock region - Added mumbai region

Signed-off-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>

* Update amazonbedrock.go

Signed-off-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>

* Added new AI model to work for ap-south-1 region[that does not uses inference profile]

Signed-off-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>

---------

Signed-off-by: Sakshi Singh <66963254+sakshirajput02@users.noreply.github.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-04 11:45:05 +01:00
renovate[bot]
e4861e9e2d fix(deps): update module gopkg.in/yaml.v2 to v3 (#1363)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 16:19:03 +01:00
Kay Yan
3c353b0e93 fix: prometheus UTF8Validation (#1404)
Signed-off-by: Kay Yan <kay.yan@daocloud.io>
2025-04-02 16:07:32 +01:00
renovate[bot]
c823de12e6 chore(deps): update goreleaser/goreleaser-action digest to 9c156ee (#1411)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 07:34:51 +01:00
renovate[bot]
e231032e1b chore(deps): pin golangci/golangci-lint-action action to 1481404 (#1415)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 20:14:41 +01:00
Alex Jones
f0b18cfb1c chore: linter (#1414)
* chore: changing linter

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

* chore: changing linter

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

* chore: changing linter

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

---------

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2025-03-31 19:17:55 +01:00
Hadi Azaddel
a31d07c802 docs: add table of contents and cleanup (#1413)
Signed-off-by: hadi2f244 <m.h.azaddel@gmail.com>
2025-03-31 19:04:59 +01:00
Eshaan Aggarwal
06d201ca5d test: add tests for k8sgpt/pkg/analyzer/events.go (#913)
* test: add tests for events_test.go

Signed-off-by: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com>

* feat: fixed event tests

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

---------

Signed-off-by: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com>
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-03-31 14:33:02 +01:00
Qian_Xiao
a962741220 docs: remove extra dollar sign in README.md (#1410)
Signed-off-by: Qian_Xiao <heyheyco@gmail.com>
2025-03-31 14:09:41 +01:00
86 changed files with 8220 additions and 1192 deletions

View File

@@ -8,13 +8,16 @@ on:
pull_request:
branches:
- 'main'
- fix/build-branch
- '[0-9]+.[1-9][0-9]*.x'
paths-ignore:
- "**.md"
env:
GO_VERSION: "~1.22"
GO_VERSION: "~1.23"
IMAGE_NAME: "k8sgpt"
REGISTRY_IMAGE: ghcr.io/k8sgpt-ai/k8sgpt
defaults:
run:
shell: bash
@@ -33,7 +36,7 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Extract branch name
id: extract_branch
@@ -51,97 +54,61 @@ jobs:
id: get_run_type
run: |
NON_FORKED_AND_NON_ROBOT_RUN=${{ ( github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]' ) && ( github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository ) }}
echo "github.actor != 'renovate[bot]' = ${{ github.actor != 'renovate[bot]' }}"
echo "github.actor != 'dependabot[bot]' = ${{ github.actor != 'dependabot[bot]' }}"
echo "github.event_name == 'push' = ${{ github.event_name == 'push' }}"
echo "github.event.pull_request.head.repo.full_name == github.repository = ${{ github.event.pull_request.head.repo.full_name == github.repository }}"
echo "NON_FORKED_AND_NON_ROBOT_RUN = $NON_FORKED_AND_NON_ROBOT_RUN"
echo "NON_FORKED_AND_NON_ROBOT_RUN=$NON_FORKED_AND_NON_ROBOT_RUN" >> "$GITHUB_OUTPUT"
build_image:
name: Build Container Image
build-and-push:
name: Build and Push Multi-arch Image
needs: prepare_ci_run
runs-on: ubuntu-latest
if: ${{ needs.prepare_ci_run.outputs.NON_FORKED_AND_NON_ROBOT_RUN == 'true' }}
env:
BRANCH: ${{ needs.prepare_ci_run.outputs.BRANCH }}
DATETIME: ${{ needs.prepare_ci_run.outputs.DATETIME }}
BUILD_TIME: ${{ needs.prepare_ci_run.outputs.BUILD_TIME }}
GIT_SHA: ${{ needs.prepare_ci_run.outputs.GIT_SHA }}
RELEASE_REGISTRY: "localhost:5000/k8sgpt"
steps:
- name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
- name: Build Docker Image
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
- name: Docker meta
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
with:
context: .
platforms: linux/amd64
file: ./container/Dockerfile
target: production
images: ${{ env.REGISTRY_IMAGE }}
tags: |
${{ env.RELEASE_REGISTRY }}/${{ env.IMAGE_NAME }}:dev-${{ env.DATETIME }}
build-args: |
GIT_HASH=${{ env.GIT_SHA }}
RELEASE_VERSION=dev-${{ env.DATETIME }}
BUILD_TIME=${{ env.BUILD_TIME }}
builder: ${{ steps.buildx.outputs.name }}
push: false
cache-from: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_NAME }}
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_NAME }}
outputs: type=docker,dest=/tmp/${{ env.IMAGE_NAME }}-image.tar
- name: Upload image as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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-latest
env:
DATETIME: ${{ needs.prepare_ci_run.outputs.DATETIME }}
BUILD_TIME: ${{ needs.prepare_ci_run.outputs.BUILD_TIME }}
GIT_SHA: ${{ needs.prepare_ci_run.outputs.GIT_SHA }}
permissions:
packages: write # Needed for pushing images to the registry
contents: read # Needed for checking out the repository
steps:
- name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=dev-${{ env.DATETIME }}
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
with:
registry: "ghcr.io"
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.K8SGPT_BOT_SECRET }}
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: Build Docker Image
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
- name: Build and push multi-arch image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
file: ./container/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
target: production
tags: |
ghcr.io/k8sgpt-ai/${{ env.IMAGE_NAME }}:dev-${{ env.DATETIME }}
build-args: |
GIT_HASH=${{ env.GIT_SHA }}
RELEASE_VERSION=dev-${{ env.DATETIME }}
BUILD_TIME=${{ env.BUILD_TIME }}
builder: ${{ steps.buildx.outputs.name }}
push: true
cache-from: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_NAME }}
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_NAME }}
tags: |
${{ env.REGISTRY_IMAGE }}:${{ env.DATETIME }}
labels: ${{ steps.meta.outputs.labels }}
secrets: |
GIT_AUTH_TOKEN=${{ secrets.K8SGPT_BOT_SECRET }}

View File

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

View File

@@ -23,7 +23,7 @@ jobs:
# Release-please creates a PR that tracks all changes
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1
id: release
@@ -55,17 +55,17 @@ jobs:
docker-images: true
swap-storage: true
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
with:
go-version: '1.22'
- name: Download Syft
uses: anchore/sbom-action/download-syft@55dc4ee22412511ee8c3142cbea40418e6cec693 # v0.17.8
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
@@ -73,8 +73,9 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.K8SGPT_BOT_SECRET }}
- name: Update new version in krew-index
uses: rajatjindal/krew-release-bot@3d9faef30a82761d610544f62afddca00993eef9 # v0.0.47
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
# - name: Update new version in krew-index
# uses: rajatjindal/krew-release-bot@3d9faef30a82761d610544f62afddca00993eef9 # v0.0.47
build-container:
if: needs.release-please.outputs.releases_created == 'true'
@@ -90,23 +91,23 @@ jobs:
IMAGE_NAME: k8sgpt
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
submodules: recursive
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.K8SGPT_BOT_SECRET }}
- name: Build Docker Image
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
file: ./container/Dockerfile
@@ -127,7 +128,7 @@ jobs:
output-file: ./sbom-${{ env.IMAGE_NAME }}.spdx.json
- name: Attach SBOM to release
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2
with:
tag_name: ${{ needs.release-please.outputs.tag_name }}
files: ./sbom-${{ env.IMAGE_NAME }}.spdx.json

View File

@@ -16,7 +16,7 @@ jobs:
pull-requests: read # Needed for reading prs
steps:
- name: Validate Pull Request
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
uses: amannn/action-semantic-pull-request@fdd4d3ddf614fbcd8c29e4b106d3bbe0cb2c605d # v6.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

View File

@@ -15,16 +15,16 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: Set up Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # 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@0565863a31f2c772f9f0395002a31e3f06189574 # v5
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

1
.gitignore vendored
View File

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

View File

@@ -70,8 +70,28 @@ checksum:
snapshot:
name_template: "{{ incpatch .Version }}-next"
# skip: true
# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
announce:
slack:
# Whether its enabled or not.
#
# Templates: allowed (since v2.6).
enabled: true
# Message template to use while publishing.
#
# Default: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'.
# Templates: allowed.
message_template: "{{ .ProjectName }} release {{.Tag}} is out!"
# The name of the channel that the user selected as a destination for webhook messages.
channel: "#general"
# Set your Webhook's user name.
username: "K8sGPT"
# Emoji to use as the icon for this message. Overrides icon_url.
icon_emoji: ""
# URL to an image to use as the icon for this message.
icon_url: ""

View File

@@ -1 +1 @@
{".":"0.4.2"}
{".":"0.4.24"}

View File

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

View File

@@ -85,6 +85,12 @@ docker-build:
@echo "===========> Building docker image"
docker buildx build --build-arg=VERSION="$$(git describe --tags --abbrev=0)" --build-arg=COMMIT="$$(git rev-parse --short HEAD)" --build-arg DATE="$$(date +%FT%TZ)" --platform="linux/amd64,linux/arm64" -t ${IMG} -f container/Dockerfile . --push
## docker-build-local: Build docker image for local testing
.PHONY: docker-build-local
docker-build-local:
@echo "===========> Building docker image for local testing"
docker build --build-arg=VERSION="$$(git describe --tags --abbrev=0)" --build-arg=COMMIT="$$(git rev-parse --short HEAD)" --build-arg DATE="$$(date +%FT%TZ)" -t k8sgpt:local -f container/Dockerfile .
## fmt: Run go fmt against code.
.PHONY: fmt
fmt:

175
README.md
View File

@@ -24,14 +24,27 @@ _Out of the box integration with OpenAI, Azure, Cohere, Amazon Bedrock, Google G
<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; />
<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)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [Community](#community)
- [License](#license)
# CLI Installation
### Linux/Mac via brew
```sh
$ brew install k8sgpt
brew install k8sgpt
```
or
@@ -49,7 +62,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.2/k8sgpt_386.rpm
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.24/k8sgpt_386.rpm
```
<!---x-release-please-end-->
@@ -57,7 +70,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.2/k8sgpt_amd64.rpm
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.24/k8sgpt_amd64.rpm
```
<!---x-release-please-end-->
</details>
@@ -70,7 +83,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.2/k8sgpt_386.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.24/k8sgpt_386.deb
sudo dpkg -i k8sgpt_386.deb
```
@@ -81,7 +94,7 @@ sudo dpkg -i k8sgpt_386.deb
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.2/k8sgpt_amd64.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.24/k8sgpt_amd64.deb
sudo dpkg -i k8sgpt_amd64.deb
```
@@ -96,7 +109,7 @@ sudo dpkg -i k8sgpt_amd64.deb
<!---x-release-please-start-version-->
```
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.2/k8sgpt_386.apk
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.24/k8sgpt_386.apk
apk add --allow-untrusted k8sgpt_386.apk
```
<!---x-release-please-end-->
@@ -105,7 +118,7 @@ sudo dpkg -i k8sgpt_amd64.deb
<!---x-release-please-start-version-->
```
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.2/k8sgpt_amd64.apk
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.24/k8sgpt_amd64.apk
apk add --allow-untrusted k8sgpt_amd64.apk
```
<!---x-release-please-end-->
@@ -133,7 +146,7 @@ If you install gcc as suggested, the problem will persist. Therefore, you need t
- Download the latest Windows binaries of **k8sgpt** from the [Release](https://github.com/k8sgpt-ai/k8sgpt/releases)
tab based on your system architecture.
- Extract the downloaded package to your desired location. Configure the system _path_ variable with the binary location
- Extract the downloaded package to your desired location. Configure the system _PATH_ environment variable with the binary location
## Operator Installation
@@ -152,6 +165,76 @@ _This mode of operation is ideal for continuous monitoring of your cluster and c
- And use `k8sgpt analyze --explain` to get a more detailed explanation of the issues.
- You also run `k8sgpt analyze --with-doc` (with or without the explain flag) to get the official documentation from Kubernetes.
# Using with Claude Desktop
K8sGPT can be integrated with Claude Desktop to provide AI-powered Kubernetes cluster analysis. This integration requires K8sGPT v0.4.14 or later.
## Prerequisites
1. Install K8sGPT v0.4.14 or later:
```sh
brew install k8sgpt
```
2. Install Claude Desktop from the official website
3. Configure K8sGPT with your preferred AI backend:
```sh
k8sgpt auth
```
## Setup
1. Start the K8sGPT MCP server:
```sh
k8sgpt serve --mcp
```
2. In Claude Desktop:
- Open Settings
- Navigate to the Integrations section
- Add K8sGPT as a new integration
- The MCP server will be automatically detected
3. Configure Claude Desktop with the following JSON:
```json
{
"mcpServers": {
"k8sgpt": {
"command": "k8sgpt",
"args": [
"serve",
"--mcp"
]
}
}
}
```
## Usage
Once connected, you can use Claude Desktop to:
- Analyze your Kubernetes cluster
- Get detailed insights about cluster health
- Receive recommendations for fixing issues
- Query cluster information
Example commands in Claude Desktop:
- "Analyze my Kubernetes cluster"
- "What's the health status of my cluster?"
- "Show me any issues in the default namespace"
## Troubleshooting
If you encounter connection issues:
1. Ensure K8sGPT is running with the MCP server enabled
2. Verify your Kubernetes cluster is accessible
3. Check that your AI backend is properly configured
4. Restart both K8sGPT and Claude Desktop
For more information, visit our [documentation](https://docs.k8sgpt.ai).
## Analyzers
K8sGPT uses analyzers to triage and diagnose issues in your cluster. It has a set of analyzers that are built in, but
@@ -169,10 +252,12 @@ you will be able to write your own analyzers.
- [x] ingressAnalyzer
- [x] statefulSetAnalyzer
- [x] deploymentAnalyzer
- [x] jobAnalyzer
- [x] cronJobAnalyzer
- [x] nodeAnalyzer
- [x] mutatingWebhookAnalyzer
- [x] validatingWebhookAnalyzer
- [x] configMapAnalyzer
#### Optional
@@ -183,6 +268,16 @@ you will be able to write your own analyzers.
- [x] gateway
- [x] httproute
- [x] logAnalyzer
- [x] storageAnalyzer
- [x] securityAnalyzer
- [x] CatalogSource
- [x] ClusterCatalog
- [x] ClusterExtension
- [x] ClusterService
- [x] ClusterServiceVersion
- [x] OperatorGroup
- [x] InstallPlan
- [x] Subscription
## Examples
@@ -304,6 +399,26 @@ _Serve mode_
k8sgpt serve
```
_Serve mode with MCP (Model Context Protocol)_
```
# Enable MCP server on default port 8089
k8sgpt serve --mcp --mcp-http
# Enable MCP server on custom port
k8sgpt serve --mcp --mcp-http --mcp-port 8089
# Full serve mode with MCP
k8sgpt serve --mcp --mcp-http --port 8080 --metrics-port 8081 --mcp-port 8089
```
The MCP server enables integration with tools like Claude Desktop and other MCP-compatible clients. It runs on port 8089 by default and provides:
- Kubernetes cluster analysis via MCP protocol
- Resource information and health status
- AI-powered issue explanations and recommendations
For Helm chart deployment with MCP support, see the `charts/k8sgpt/values-mcp-example.yaml` file.
_Analysis with serve mode_
```
@@ -324,14 +439,14 @@ _Print analysis stats_
```
k8sgpt analyze -s
The stats mode allows for debugging and understanding the time taken by an analysis by displaying the statistics of each analyzer.
- Analyzer Ingress took 47.125583ms
- Analyzer PersistentVolumeClaim took 53.009167ms
- Analyzer CronJob took 57.517792ms
- Analyzer Deployment took 156.6205ms
- Analyzer Node took 160.109833ms
- Analyzer ReplicaSet took 245.938333ms
- Analyzer StatefulSet took 448.0455ms
- Analyzer Pod took 5.662594708s
- Analyzer Ingress took 47.125583ms
- Analyzer PersistentVolumeClaim took 53.009167ms
- Analyzer CronJob took 57.517792ms
- Analyzer Deployment took 156.6205ms
- Analyzer Node took 160.109833ms
- Analyzer ReplicaSet took 245.938333ms
- Analyzer StatefulSet took 448.0455ms
- Analyzer Pod took 5.662594708s
- Analyzer Service took 38.583359166s
```
@@ -380,6 +495,22 @@ k8sgpt auth default -p azureopenai
Default provider set to azureopenai
```
_Using Amazon Bedrock with inference profiles_
_System Inference Profile_
```
k8sgpt auth add --backend amazonbedrock --providerRegion us-east-1 --model arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-inference-profile
```
_Application Inference Profile_
```
k8sgpt auth add --backend amazonbedrock --providerRegion us-east-1 --model arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/2uzp4s0w39t6
```
## Key Features
<details>
@@ -412,11 +543,9 @@ The Kubernetes system is trying to scale a StatefulSet named tGLcCRcHa1Ce5Rs usi
The Kubernetes system is trying to scale a StatefulSet named fake-deployment using the HorizontalPodAutoscaler, but it cannot find the StatefulSet. The solution is to verify that the StatefulSet name is spelled correctly and exists in the same namespace as the HorizontalPodAutoscaler.
```
Note: **Anonymization does not currently apply to events.**
### Further Details
**Anonymization does not currently apply to events.**
Note: **Anonymization does not currently apply to events.**
_In a few analysers like Pod, we feed to the AI backend the event messages which are not known beforehand thus we are not masking them for the **time being**._
@@ -434,7 +563,7 @@ _In a few analysers like Pod, we feed to the AI backend the event messages which
- The following is the list of analysers in which data is **not being masked**:-
- RepicaSet
- ReplicaSet
- PersistentVolumeClaim
- Pod
- Log
@@ -485,7 +614,7 @@ There may be scenarios where caching remotely is preferred.
In these scenarios K8sGPT supports AWS S3 or Azure Blob storage Integration.
<summary> Remote caching </summary>
<em>Note: You can only configure and use only one remote cache at a time</em>
<em>Note: You can configure and use only one remote cache at a time</em>
_Adding a remote cache_
@@ -581,7 +710,7 @@ Please read our [contributing guide](./CONTRIBUTING.md).
## Community
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-276pa9uyq-pxAUr4TCVHubFxEvLZuT1Q)
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-332vhyaxv-bfjJwHZLXWVCB3QaXafEYQ)
<a href="https://github.com/k8sgpt-ai/k8sgpt/graphs/contributors">
<img src="https://contrib.rocks/image?repo=k8sgpt-ai/k8sgpt" />

80
SUPPORTED_MODELS.md Normal file
View File

@@ -0,0 +1,80 @@
# 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
---
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.3.0 #x-release-please-version
appVersion: v0.4.23 #x-release-please-version
description: A Helm chart for K8SGPT
name: k8sgpt
type: application

View File

@@ -32,7 +32,13 @@ spec:
image: {{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default .Chart.AppVersion }}
ports:
- containerPort: 8080
args: ["serve"]
{{- if .Values.deployment.mcp.enabled }}
- containerPort: {{ .Values.deployment.mcp.port | int }}
{{- end }}
args: ["serve"
{{- if .Values.deployment.mcp.enabled }}, "--mcp", "-v","--mcp-http", "--mcp-port", {{ .Values.deployment.mcp.port | quote }}
{{- end }}
]
{{- if .Values.deployment.resources }}
resources:
{{- toYaml .Values.deployment.resources | nindent 10 }}

View File

@@ -19,4 +19,9 @@ spec:
- name: metrics
port: 8081
targetPort: 8081
{{- if .Values.deployment.mcp.enabled }}
- name: mcp
port: {{ .Values.deployment.mcp.port | int }}
targetPort: {{ .Values.deployment.mcp.port | int }}
{{- end }}
type: {{ .Values.service.type }}

View File

@@ -0,0 +1,39 @@
# Example values file to enable MCP (Model Context Protocol) service
# Copy this file and modify as needed, then use: helm install -f values-mcp-example.yaml
deployment:
# Enable MCP server
mcp:
enabled: true
port: "8089" # Port for MCP server (default: 8089)
http: true # Enable HTTP mode for MCP server
# Other deployment settings remain the same
image:
repository: ghcr.io/k8sgpt-ai/k8sgpt
tag: "" # defaults to Chart.appVersion if unspecified
imagePullPolicy: Always
env:
model: "gpt-3.5-turbo"
backend: "openai"
resources:
limits:
cpu: "1"
memory: "512Mi"
requests:
cpu: "0.2"
memory: "156Mi"
# Service configuration
service:
type: ClusterIP
annotations: {}
# Secret configuration for AI backend
secret:
secretKey: "" # base64 encoded OpenAI token
# ServiceMonitor for Prometheus metrics
serviceMonitor:
enabled: false
additionalLabels: {}

View File

@@ -7,6 +7,11 @@ deployment:
env:
model: "gpt-3.5-turbo"
backend: "openai" # one of: [ openai | llama ]
# MCP (Model Context Protocol) server configuration
mcp:
enabled: false # Enable MCP server
port: "8089" # Port for MCP server
http: true # Enable HTTP mode for MCP server
resources:
limits:
cpu: "1"

View File

@@ -23,6 +23,7 @@ import (
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/interactive"
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
@@ -67,25 +68,45 @@ var AnalyzeCmd = &cobra.Command{
withStats,
)
verbose := viper.GetBool("verbose")
if verbose {
fmt.Println("Debug: Checking analysis configuration.")
}
if err != nil {
color.Red("Error: %v", err)
os.Exit(1)
}
if verbose {
fmt.Println("Debug: Analysis initialized.")
}
defer config.Close()
if customAnalysis {
config.RunCustomAnalysis()
if verbose {
fmt.Println("Debug: All custom analyzers completed.")
}
}
config.RunAnalysis()
if verbose {
fmt.Println("Debug: All core analyzers completed.")
}
if explain {
if err := config.GetAIResults(output, anonymize); err != nil {
err := config.GetAIResults(output, anonymize)
if verbose {
fmt.Println("Debug: Checking AI results.")
}
if err != nil {
color.Red("Error: %v", err)
os.Exit(1)
}
}
// print results
output_data, err := config.PrintOutput(output)
if verbose {
fmt.Println("Debug: Checking output.")
}
if err != nil {
color.Red("Error: %v", err)
os.Exit(1)

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

43
cmd/cache/purge.go vendored
View File

@@ -23,23 +23,51 @@ import (
"github.com/spf13/cobra"
)
var all bool
var purgeCmd = &cobra.Command{
Use: "purge [object name]",
Short: "Purge a remote cache",
Long: "This command allows you to delete/purge one object from the cache",
Long: "This command allows you to delete/purge one object from the cache or all objects with --all flag.",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
color.Red("Error: Please provide a value for object name. Run k8sgpt cache purge --help")
os.Exit(1)
}
objectKey := args[0]
fmt.Println(color.YellowString("Purging a remote cache."))
c, err := cache.GetCacheConfiguration()
if err != nil {
color.Red("Error: %v", err)
os.Exit(1)
}
if all {
fmt.Println(color.YellowString("Purging all objects from the remote cache."))
names, err := c.List()
if err != nil {
color.Red("Error listing cache objects: %v", err)
os.Exit(1)
}
if len(names) == 0 {
fmt.Println(color.GreenString("No objects to delete."))
return
}
var failed []string
for _, obj := range names {
err := c.Remove(obj.Name)
if err != nil {
failed = append(failed, obj.Name)
}
}
if len(failed) > 0 {
color.Red("Failed to delete: %v", failed)
os.Exit(1)
}
fmt.Println(color.GreenString("All objects deleted."))
return
}
if len(args) == 0 {
color.Red("Error: Please provide a value for object name or use --all. Run k8sgpt cache purge --help")
os.Exit(1)
}
objectKey := args[0]
fmt.Println(color.YellowString("Purging a remote cache."))
err = c.Remove(objectKey)
if err != nil {
color.Red("Error: %v", err)
@@ -50,5 +78,6 @@ var purgeCmd = &cobra.Command{
}
func init() {
purgeCmd.Flags().BoolVar(&all, "all", false, "Purge all objects in the cache")
CacheCmd.AddCommand(purgeCmd)
}

View File

@@ -37,6 +37,7 @@ var (
cfgFile string
kubecontext string
kubeconfig string
verbose bool
Version string
Commit string
Date string
@@ -84,6 +85,7 @@ func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", fmt.Sprintf("Default config file (%s/k8sgpt/k8sgpt.yaml)", xdg.ConfigHome))
rootCmd.PersistentFlags().StringVar(&kubecontext, "kubecontext", "", "Kubernetes context to use. Only required if out-of-cluster.")
rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show detailed tool actions (e.g., API calls, checks).")
}
// initConfig reads in config file and ENV variables if set.
@@ -104,6 +106,7 @@ func initConfig() {
viper.Set("kubecontext", kubecontext)
viper.Set("kubeconfig", kubeconfig)
viper.Set("verbose", verbose)
viper.SetEnvPrefix("K8SGPT")
viper.AutomaticEnv() // read in environment variables that match

30
cmd/root_test.go Normal file
View File

@@ -0,0 +1,30 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"testing"
"github.com/spf13/viper"
)
// Test that verbose flag is correctly set in viper.
func TestInitConfig_VerboseFlag(t *testing.T) {
verbose = true
viper.Reset()
initConfig()
if !viper.GetBool("verbose") {
t.Error("Expected verbose flag to be true")
}
}

View File

@@ -38,6 +38,9 @@ var (
metricsPort string
backend string
enableHttp bool
enableMCP bool
mcpPort string
mcpHTTP bool
)
var ServeCmd = &cobra.Command{
@@ -183,6 +186,21 @@ 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)
}
}()
}
server := k8sgptserver.Config{
Backend: aiProvider.Name,
Port: port,
@@ -216,4 +234,7 @@ func init() {
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")
}

155
go.mod
View File

@@ -13,12 +13,12 @@ require (
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.10.0
golang.org/x/term v0.29.0
helm.sh/helm/v3 v3.16.3
k8s.io/api v0.31.3
k8s.io/apimachinery v0.31.3
k8s.io/client-go v0.31.3
k8s.io/kubectl v0.31.1 // indirect
golang.org/x/term v0.30.0
helm.sh/helm/v3 v3.17.4
k8s.io/api v0.32.2
k8s.io/apimachinery v0.32.2
k8s.io/client-go v0.32.2
k8s.io/kubectl v0.32.2 // indirect
)
@@ -32,21 +32,27 @@ require (
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1
cloud.google.com/go/storage v1.48.0
cloud.google.com/go/vertexai v0.13.2
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0
github.com/IBM/watsonx-go v1.0.1
github.com/aws/aws-sdk-go v1.55.5
github.com/agiledragon/gomonkey/v2 v2.13.0
github.com/aws/aws-sdk-go v1.55.7
github.com/aws/aws-sdk-go-v2 v1.36.3
github.com/aws/aws-sdk-go-v2/config v1.29.14
github.com/aws/aws-sdk-go-v2/service/bedrock v1.33.0
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0
github.com/cohere-ai/cohere-go/v2 v2.12.2
github.com/go-logr/zapr v1.3.0
github.com/google/generative-ai-go v0.19.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1
github.com/hupe1980/go-huggingface v0.0.15
github.com/kyverno/policy-reporter-kyverno-plugin v1.6.4
github.com/mark3labs/mcp-go v0.36.0
github.com/olekukonko/tablewriter v0.0.5
github.com/oracle/oci-go-sdk/v65 v65.79.0
github.com/prometheus/prometheus v0.300.1
github.com/prometheus/prometheus v0.302.1
github.com/pterm/pterm v0.12.80
google.golang.org/api v0.210.0
google.golang.org/api v0.218.0
gopkg.in/yaml.v2 v2.4.0
sigs.k8s.io/controller-runtime v0.19.3
sigs.k8s.io/gateway-api v1.2.1
@@ -56,28 +62,40 @@ require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
cel.dev/expr v0.16.1 // indirect
cel.dev/expr v0.19.0 // indirect
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/ai v0.8.0 // indirect
cloud.google.com/go/aiplatform v1.69.0 // indirect
cloud.google.com/go/auth v0.11.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
cloud.google.com/go/compute/metadata v0.5.2 // indirect
cloud.google.com/go/auth v0.14.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.2.2 // indirect
cloud.google.com/go/longrunning v0.6.2 // indirect
cloud.google.com/go/monitoring v1.21.2 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // 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/hcsshim v0.12.4 // indirect
github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 // indirect
github.com/aws/aws-sdk-go-v2 v1.32.3 // indirect
github.com/aws/smithy-go v1.22.0 // indirect
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
github.com/containerd/console v1.0.4 // indirect
@@ -88,22 +106,23 @@ require (
github.com/creack/pty v1.1.21 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/envoyproxy/go-control-plane v0.13.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
github.com/envoyproxy/go-control-plane v0.13.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/expr-lang/expr v1.16.9 // indirect
github.com/expr-lang/expr v1.17.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // 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/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
@@ -113,27 +132,30 @@ require (
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/prometheus/common/sigv4 v0.1.0 // indirect
github.com/prometheus/sigv4 v0.1.1 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/segmentio/fasthash v1.0.3 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/sony/gobreaker v0.5.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/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
go.opentelemetry.io/otel/metric v1.31.0 // indirect
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect
@@ -152,19 +174,19 @@ require (
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.23 // indirect
github.com/cyphar/filepath-securejoin v0.3.4 // indirect
github.com/containerd/containerd v1.7.24 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v26.1.4+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v27.3.1+incompatible // indirect
github.com/docker/docker v27.4.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.7.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
@@ -188,12 +210,11 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.10 // indirect
github.com/klauspost/compress v1.17.11 // 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
@@ -208,7 +229,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.4.0 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -220,58 +241,58 @@ require (
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.20.5
github.com/prometheus/client_golang v1.21.0-rc.0
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.1 // indirect
github.com/prometheus/common v0.62.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.7.0 // indirect
github.com/rubenv/sql-migrate v1.7.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/otel v1.31.0 // indirect
go.opentelemetry.io/otel/trace v1.31.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
golang.org/x/net v0.36.0
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.8.0 // indirect
google.golang.org/grpc v1.68.1
google.golang.org/protobuf v1.35.2 // indirect
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
k8s.io/apiextensions-apiserver v0.31.3
k8s.io/apiserver v0.31.3 // indirect
k8s.io/cli-runtime v0.31.1 // indirect
k8s.io/component-base v0.31.3 // indirect
k8s.io/apiextensions-apiserver v0.32.2
k8s.io/apiserver v0.32.2 // indirect
k8s.io/cli-runtime v0.32.2 // indirect
k8s.io/component-base v0.32.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 // indirect
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
oras.land/oras-go v1.2.5 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/kustomize/api v0.18.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
// v1.2.0 is taken from github.com/open-policy-agent/opa v0.42.0
// v1.2.0 incompatible with github.com/docker/docker v23.0.0-rc.1+incompatible
//replace oras.land/oras-go => oras.land/oras-go v1.2.4
replace github.com/docker/docker => github.com/docker/docker v27.4.0+incompatible
replace github.com/docker/docker => github.com/docker/docker v28.0.4+incompatible
replace dario.cat/mergo => github.com/imdario/mergo v1.0.1

395
go.sum
View File

@@ -16,8 +16,8 @@ buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1 h
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.5.1-20241118152629-1379a5a1889d.1/go.mod h1:rlbkTkVN2P3aNR0U/7N5d9/uvNW8/dzHwtJDfPzh2vc=
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1 h1:Z+fW0kWryP6LdjP+z+d1/WT4tObrq890aye4aPIh6hM=
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.35.2-20241118152629-1379a5a1889d.1/go.mod h1:dqopmdpTDT6p9kPTxVCgR8WDnNb1SjZjwzaNj/kRbps=
cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g=
cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8=
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -123,10 +123,10 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo
cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo=
cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0=
cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E=
cloud.google.com/go/auth v0.11.0 h1:Ic5SZz2lsvbYcWT5dfjNWgw6tTlGi2Wc8hyQSC9BstA=
cloud.google.com/go/auth v0.11.0/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=
cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=
cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8=
@@ -205,8 +205,8 @@ cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZ
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY=
cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck=
cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w=
@@ -644,12 +644,12 @@ gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zum
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.7.0 h1:LkHbJbgF3YyvC53aqYGR+wWQDn2Rdp9AQdGndf9QvY4=
@@ -664,8 +664,8 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.3 h1:6LyjnnaLpcOKK0fbYisI+mb8CE7iNe7i89nMNQxFxs8=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
@@ -674,8 +674,8 @@ github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U
github.com/Code-Hex/go-generics-cache v1.5.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 h1:pB2F2JKCj1Znmp2rwxxt1J0Fg0wezTMgWYk5Mpbi1kg=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo=
@@ -713,17 +713,16 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/agiledragon/gomonkey/v2 v2.13.0 h1:B24Jg6wBI1iB8EFR1c+/aoTg7QN/Cum7YffG8KMIyYo=
github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30 h1:t3eaIm0rUkzbrIewtiFmMK5RXHej2XnoXNhxVsAYUfg=
github.com/alecthomas/units v0.0.0-20240626203959-61d1e3462e30/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
@@ -736,13 +735,42 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk=
github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/bedrock v1.33.0 h1:2P70khV5KDzoRs8UuplU3rAzzyLaj5kzND33Jutwpbg=
github.com/aws/aws-sdk-go-v2/service/bedrock v1.33.0/go.mod h1:rZOgAxQVRg9v5ZEQHrrKw0Gkb9DBAASeeRiwUmmXcG0=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0 h1:eMOwQ8ZZK+76+08RfxeaGUtRFN6wxmD1rvqovc2kq2w=
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.30.0/go.mod h1:0b5Rq7rUvSQFYHI1UO0zFTV/S6j6DUyuykXA80C+YOI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -753,6 +781,7 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
@@ -794,13 +823,13 @@ github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1Ig
github.com/cohere-ai/cohere-go/v2 v2.12.2 h1:8WJqqcCe3q6TB1CdhgzJOgRO2ouno8xcYcOoeWtI8Pk=
github.com/cohere-ai/cohere-go/v2 v2.12.2/go.mod h1:MuiJkCxlR18BDV2qQPbz2Yb/OCVphT1y6nD2zYaKeR0=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0=
github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0=
github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ=
github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA=
github.com/containerd/containerd v1.7.24/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8=
github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
@@ -813,16 +842,16 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/digitalocean/godo v1.126.0 h1:+Znh7VMQj/E8ArbjWnc7OKGjWfzC+I8OCSRp7r1MdD8=
github.com/digitalocean/godo v1.126.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=
github.com/digitalocean/godo v1.132.0 h1:n0x6+ZkwbyQBtIU1wwBhv26EINqHg0wWQiBXlwYg/HQ=
github.com/digitalocean/godo v1.132.0/go.mod h1:PU8JB6I1XYkQIdHFop8lLAY9ojp6M0XcU0TWaQSxbrc=
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc=
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -831,8 +860,8 @@ github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwen
github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v27.4.0+incompatible h1:I9z7sQ5qyzO0BfAb9IMOawRkAGxhYsidKiTMcm0DU+A=
github.com/docker/docker v27.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@@ -859,21 +888,21 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34=
github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les=
github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8=
github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso=
github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -885,8 +914,8 @@ github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7Dlme
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -903,13 +932,10 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -925,8 +951,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E=
github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -943,8 +969,8 @@ github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeH
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
@@ -1042,11 +1068,11 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -1071,8 +1097,8 @@ github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqE
github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o=
github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
@@ -1080,7 +1106,9 @@ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=
github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/gophercloud/gophercloud/v2 v2.4.0 h1:XhP5tVEH3ni66NSNK1+0iSO6kaGPH/6srtx6Cr+8eCg=
github.com/gophercloud/gophercloud/v2 v2.4.0/go.mod h1:uJWNpTgJPSl2gyzJqcU/pIAhFUWvIkp8eE8M15n9rs4=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
@@ -1096,10 +1124,10 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:Fecb
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/hashicorp/consul/api v1.29.4 h1:P6slzxDLBOxUSj3fWo2o65VuKtbtOXFi7TSSgtXutuE=
github.com/hashicorp/consul/api v1.29.4/go.mod h1:HUlfw+l2Zy68ceJavv2zAyArl2fqhGWnMycyt56sBgg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hashicorp/consul/api v1.31.0 h1:32BUNLembeSRek0G/ZAM6WNfdEwYdYo8oQ4+JoqGkNQ=
github.com/hashicorp/consul/api v1.31.0/go.mod h1:2ZGIiXM3A610NmDULmCHd/aqBJj8CkMfOhswhOafxRg=
github.com/hashicorp/cronexpr v1.1.2 h1:wG/ZYIKT+RT3QkOdgYc+xsKWVRgnxJ1OJtjjy84fJ9A=
github.com/hashicorp/cronexpr v1.1.2/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -1123,12 +1151,12 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/nomad/api v0.0.0-20240717122358-3d93bd3778f3 h1:fgVfQ4AC1avVOnu2cfms8VAiD8lUq3vWI8mTocOXN/w=
github.com/hashicorp/nomad/api v0.0.0-20240717122358-3d93bd3778f3/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE=
github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec h1:+YBzb977VrmffaCX/OBm17dEVJUcWn5dW+eqs3aIJ/A=
github.com/hashicorp/nomad/api v0.0.0-20241218080744-e3ac00f30eec/go.mod h1:svtxn6QnrQ69P23VvIWMR34tg3vmwLz4UdUzm1dSCgE=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/hetznercloud/hcloud-go/v2 v2.13.1 h1:jq0GP4QaYE5d8xR/Zw17s9qoaESRJMXfGmtD1a/qckQ=
github.com/hetznercloud/hcloud-go/v2 v2.13.1/go.mod h1:dhix40Br3fDiBhwaSG/zgaYOFFddpfBm/6R1Zz0IiF0=
github.com/hetznercloud/hcloud-go/v2 v2.18.0 h1:BemrVGeWI8Kn/pvaC1jBsHZxQMnRqOydS7Ju4BERB4Q=
github.com/hetznercloud/hcloud-go/v2 v2.18.0/go.mod h1:r5RTzv+qi8IbLcDIskTzxkFIji7Ovc8yNgepQR9M+UA=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/hupe1980/go-huggingface v0.0.15 h1:tTWmUGGunC/BYz4hrwS8SSVtMYVYjceG2uhL8HxeXvw=
@@ -1136,14 +1164,14 @@ github.com/hupe1980/go-huggingface v0.0.15/go.mod h1:IRvsik3+b9BJyw9hCfw1arI6gDO
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/imdario/mergo v1.0.1 h1:lFIgOs30GMaV/2+qQ+eEBLbUL6h1YosdohE3ODy4hTs=
github.com/imdario/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/ionos-cloud/sdk-go/v6 v6.2.1 h1:mxxN+frNVmbFrmmFfXnBC3g2USYJrl6mc1LW2iNYbFY=
github.com/ionos-cloud/sdk-go/v6 v6.2.1/go.mod h1:SXrO9OGyWjd2rZhAhEpdYN6VUAODzzqRdqA9BCviQtI=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/ionos-cloud/sdk-go/v6 v6.3.2 h1:2mUmrZZz6cPyT9IRX0T8fBLc/7XU/eTxP2Y5tS7/09k=
github.com/ionos-cloud/sdk-go/v6 v6.3.2/go.mod h1:SXrO9OGyWjd2rZhAhEpdYN6VUAODzzqRdqA9BCviQtI=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -1156,14 +1184,12 @@ github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2E
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
@@ -1175,8 +1201,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@@ -1185,7 +1211,6 @@ github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZY
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00=
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -1209,8 +1234,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/linode/linodego v1.41.0 h1:GcP7JIBr9iLRJ9FwAtb9/WCT1DuPJS/xUApapfdjtiY=
github.com/linode/linodego v1.41.0/go.mod h1:Ow4/XZ0yvWBzt3iAHwchvhSx30AyLintsSMvvQ2/SJY=
github.com/linode/linodego v1.46.0 h1:+uOG4SD2MIrhbrLrvOD5HrbdLN3D19Wgn3MgdUNQjeU=
github.com/linode/linodego v1.46.0/go.mod h1:vyklQRzZUWhFVBZdYx4dcYJU/gG9yKB9VUcUs6ub0Lk=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
@@ -1219,6 +1244,8 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis=
github.com/mark3labs/mcp-go v0.36.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
@@ -1233,8 +1260,8 @@ github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
@@ -1255,8 +1282,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g=
github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
@@ -1324,10 +1351,8 @@ github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjz
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.21.0-rc.0 h1:bR+RxBlwcr4q8hXkgSOA/J18j6n0/qH0Gb0DH+8c+RY=
github.com/prometheus/client_golang v1.21.0-rc.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -1336,22 +1361,17 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4=
github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/prometheus v0.300.1 h1:9KKcTTq80gkzmXW0Et/QCFSrBPgmwiS3Hlcxc6o8KlM=
github.com/prometheus/prometheus v0.300.1/go.mod h1:gtTPY/XVyCdqqnjA3NzDMb0/nc5H9hOu1RMame+gHyM=
github.com/prometheus/prometheus v0.302.1 h1:xqVdrwrB4WNpdgJqxsz5loqFWNUZitsK8myqLuSZ6Ag=
github.com/prometheus/prometheus v0.302.1/go.mod h1:YcyCoTbUR/TM8rY3Aoeqr0AWTu/pu1Ehh+trpX3eRzg=
github.com/prometheus/sigv4 v0.1.1 h1:UJxjOqVcXctZlwDjpUpZ2OiMWJdFijgSofwLzO1Xk0Q=
github.com/prometheus/sigv4 v0.1.1/go.mod h1:RAmWVKqx0bwi0Qm4lrKMXFM0nhpesBcenfCtz9qRyH8=
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
@@ -1373,10 +1393,10 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rubenv/sql-migrate v1.7.0 h1:HtQq1xyTN2ISmQDggnh0c9U3JlP8apWh8YO2jzlXpTI=
github.com/rubenv/sql-migrate v1.7.0/go.mod h1:S4wtDEG1CKn+0ShpTtzWhFpHHI5PvCUtiGI+C+Z2THE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4=
github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
@@ -1399,10 +1419,10 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -1413,8 +1433,8 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -1445,6 +1465,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs=
github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -1459,6 +1481,8 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1483,24 +1507,26 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/detectors/gcp v1.29.0 h1:TiaiXB4DpGD3sdzNlYQxruQngn5Apwzi1X0DRhuGvDQ=
go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0 h1:P78qWqkLSShicHmAzfECaTgvslqHxblNE9j62Ws1NK8=
go.opentelemetry.io/contrib/detectors/gcp v1.32.0/go.mod h1:TVqo0Sda4Cv8gCIixd7LuLwW4EylumVWfhjZJjDD4DU=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -1521,8 +1547,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1581,8 +1607,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1621,7 +1647,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -1642,8 +1667,8 @@ golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1672,8 +1697,8 @@ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1690,15 +1715,14 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1708,7 +1732,6 @@ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1722,8 +1745,6 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1743,7 +1764,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1781,8 +1801,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1791,8 +1811,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1809,16 +1829,16 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1827,6 +1847,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -1881,8 +1902,8 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1958,8 +1979,8 @@ google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
google.golang.org/api v0.210.0 h1:HMNffZ57OoZCRYSbdWVRoqOa8V8NIHLL0CzdBPLztWk=
google.golang.org/api v0.210.0/go.mod h1:B9XDZGnx2NtyjzVkOVTGrFSAVZgPcbedzKg/gTLwqBs=
google.golang.org/api v0.218.0 h1:x6JCjEWeZ9PFCRe9z0FBrNwj7pB7DOAqT35N+IPnAUA=
google.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -2104,12 +2125,12 @@ google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8=
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2149,11 +2170,9 @@ google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a h1:UIpYSuWdWHSzjwcAFRLjKcPXFZVVLXGEM23W+NWqipw=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a/go.mod h1:9i1T9n4ZinTUZGgzENMi8MDDgbGC5mqTS75JAv6xN3A=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -2171,8 +2190,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -2190,9 +2209,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -2201,8 +2218,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
helm.sh/helm/v3 v3.16.3 h1:kb8bSxMeRJ+knsK/ovvlaVPfdis0X3/ZhYCSFRP+YmY=
helm.sh/helm/v3 v3.16.3/go.mod h1:zeVWGDR4JJgiRbT3AnNsjYaX8OTJlIE9zC+Q7F7iUSU=
helm.sh/helm/v3 v3.17.4 h1:GK+vgn9gKCyoH44+f3B5zpA78iH3AK4ywIInDEmmn/g=
helm.sh/helm/v3 v3.17.4/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -2211,28 +2228,28 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8=
k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE=
k8s.io/apiextensions-apiserver v0.31.3 h1:+GFGj2qFiU7rGCsA5o+p/rul1OQIq6oYpQw4+u+nciE=
k8s.io/apiextensions-apiserver v0.31.3/go.mod h1:2DSpFhUZZJmn/cr/RweH1cEVVbzFw9YBu4T+U3mf1e4=
k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4=
k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/apiserver v0.31.3 h1:+1oHTtCB+OheqFEz375D0IlzHZ5VeQKX1KGXnx+TTuY=
k8s.io/apiserver v0.31.3/go.mod h1:PrxVbebxrxQPFhJk4powDISIROkNMKHibTg9lTRQ0Qg=
k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk=
k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U=
k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4=
k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs=
k8s.io/component-base v0.31.3 h1:DMCXXVx546Rfvhj+3cOm2EUxhS+EyztH423j+8sOwhQ=
k8s.io/component-base v0.31.3/go.mod h1:xME6BHfUOafRgT0rGVBGl7TuSg8Z9/deT7qq6w7qjIU=
k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4=
k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA=
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/apiserver v0.32.2 h1:WzyxAu4mvLkQxwD9hGa4ZfExo3yZZaYzoYvvVDlM6vw=
k8s.io/apiserver v0.32.2/go.mod h1:PEwREHiHNU2oFdte7BjzA1ZyjWjuckORLIK/wLV5goM=
k8s.io/cli-runtime v0.32.2 h1:aKQR4foh9qeyckKRkNXUccP9moxzffyndZAvr+IXMks=
k8s.io/cli-runtime v0.32.2/go.mod h1:a/JpeMztz3xDa7GCyyShcwe55p8pbcCVQxvqZnIwXN8=
k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA=
k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94=
k8s.io/component-base v0.32.2 h1:1aUL5Vdmu7qNo4ZsE+569PV5zFatM9hl+lb3dEea2zU=
k8s.io/component-base v0.32.2/go.mod h1:PXJ61Vx9Lg+P5mS8TLd7bCIr+eMJRQTyXe8KvkrvJq0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 h1:MErs8YA0abvOqJ8gIupA1Tz6PKXYUw34XsGlA7uSL1k=
k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094/go.mod h1:7ioBJr1A6igWjsR2fxq2EZ0mlMwYLejazSIc2bzMp2U=
k8s.io/kubectl v0.31.1 h1:ih4JQJHxsEggFqDJEHSOdJ69ZxZftgeZvYo7M/cpp24=
k8s.io/kubectl v0.31.1/go.mod h1:aNuQoR43W6MLAtXQ/Bu4GDmoHlbhHKuyD49lmTC8eJM=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/kubectl v0.32.2 h1:TAkag6+XfSBgkqK9I7ZvwtF0WVtUAvK8ZqTt+5zi1Us=
k8s.io/kubectl v0.32.2/go.mod h1:+h/NQFSPxiDZYX/WZaWw9fwYezGLISP0ud8nQKg+3g8=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 h1:uUSDGlOIkdPT4svjlhi+JEnP2Ufw7AM/F5QDYiEL02U=
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3/go.mod h1:FeMbTLlxQqSASwlRCrYEOsZ0OKUgSj52qxhECwYCJsw=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
@@ -2279,13 +2296,13 @@ sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8b
sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM=
sigs.k8s.io/gateway-api v1.2.1 h1:fZZ/+RyRb+Y5tGkwxFKuYuSRQHu9dZtbjenblleOLHM=
sigs.k8s.io/gateway-api v1.2.1/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=
sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U=
sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E=
sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@@ -3,27 +3,34 @@ package ai
import (
"context"
"errors"
"github.com/aws/aws-sdk-go/service/bedrockruntime/bedrockruntimeiface"
"fmt"
"os"
"regexp"
"strings"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/bedrockruntime"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/bedrock"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"github.com/aws/smithy-go/middleware"
smithyhttp "github.com/aws/smithy-go/transport/http"
)
const amazonbedrockAIClientName = "amazonbedrock"
// AmazonBedRockClient represents the client for interacting with the AmazonCompletion Bedrock service.
// AmazonBedRockClient represents the client for interacting with the Amazon Bedrock service.
type AmazonBedRockClient struct {
nopCloser
client bedrockruntimeiface.BedrockRuntimeAPI
client BedrockRuntimeAPI
mgmtClient BedrockManagementAPI
model *bedrock_support.BedrockModel
temperature float32
topP float32
maxTokens int
models []bedrock_support.BedrockModel
}
// AmazonCompletion BedRock support region list US East (N. Virginia),US West (Oregon),Asia Pacific (Singapore),Asia Pacific (Tokyo),Europe (Frankfurt)
@@ -37,6 +44,8 @@ const (
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{
@@ -46,187 +55,293 @@ var BEDROCKER_SUPPORTED_REGION = []string{
AP_Northeast_1,
EU_Central_1,
AP_South_1,
US_Gov_West_1,
US_Gov_East_1,
}
var (
models = []bedrock_support.BedrockModel{
{
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",
},
var defaultModels = []bedrock_support.BedrockModel{
{
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-3-5-sonnet-20241022-v2:0",
Completion: &bedrock_support.CohereCompletion{},
Response: &bedrock_support.CohereResponse{},
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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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.wamazon.nova-pro-v1: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: "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: "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: "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: "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: "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: "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: "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: "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",
},
},
}
// 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,
}
}
// GetModelOrDefault check config region
func GetRegionOrDefault(region string) string {
if os.Getenv("AWS_DEFAULT_REGION") != "" {
region = os.Getenv("AWS_DEFAULT_REGION")
}
@@ -241,40 +356,139 @@ 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) {
for _, m := range models {
if model == m.Name {
return &m, nil
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
}
}
return nil, errors.New("model not found")
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 {
// Create a new AWS session
providerRegion := GetRegionOrDefault(config.GetProviderRegion())
sess, err := session.NewSession(&aws.Config{
Region: aws.String(providerRegion),
})
if err != nil {
return err
// Initialize models if not already initialized
if a.models == nil {
a.models = defaultModels
}
foundModel, err := a.getModelFromString(config.GetModel())
if err != nil {
return err
}
// TODO: Override the completion config somehow
// Get the model input
modelInput := config.GetModel()
// Create a new BedrockRuntime client
a.client = bedrockruntime.New(sess)
a.model = foundModel
a.model.Config.ModelName = foundModel.Name
// 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 {
// Instead of using a fallback model, throw an error
return fmt.Errorf("failed to get inference profile: %v", err)
} else {
// 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 using a fallback model, throw an error
return fmt.Errorf("failed to find model configuration for %s: %v", modelID, err)
}
a.model = foundModel
// Use the inference profile ARN as the model ID for API calls
a.model.Config.ModelName = modelInput
}
} else {
// Regular model ID provided
foundModel, err := a.getModelFromString(modelInput)
if err != nil {
return fmt.Errorf("model '%s' is not supported: %v", modelInput, err)
}
a.model = foundModel
a.model.Config.ModelName = foundModel.Config.ModelName
}
// Set common configuration parameters
a.temperature = config.GetTemperature()
a.topP = config.GetTopP()
a.maxTokens = config.GetMaxTokens()
@@ -282,35 +496,128 @@ 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
}
if !bedrock_support.IsModelSupported(a.model.Config.ModelName, supportedModels) {
return "", fmt.Errorf("model '%s' is not supported.\nSupported models:\n%s", a.model.Config.ModelName, func() string {
s := ""
for _, m := range supportedModels {
s += " - " + m + "\n"
}
return s
}())
}
body, err := a.model.Completion.GetCompletion(ctx, prompt, a.model.Config)
if err != nil {
return "", err
}
// Build the parameters for the model invocation
params := &bedrockruntime.InvokeModelInput{
Body: body,
ModelId: aws.String(a.model.Name),
ModelId: aws.String(a.model.Config.ModelName),
ContentType: aws.String("application/json"),
Accept: aws.String("application/json"),
}
// Invoke the model
resp, err := a.client.InvokeModelWithContext(ctx, params)
// Detect if the model name is an inference profile ARN and set the header if so
var optFns []func(*bedrockruntime.Options)
if validateInferenceProfileArn(a.model.Config.ModelName) {
inferenceProfileArn := a.model.Config.ModelName
optFns = append(optFns, func(options *bedrockruntime.Options) {
options.APIOptions = append(options.APIOptions, func(stack *middleware.Stack) error {
return stack.Initialize.Add(middleware.InitializeMiddlewareFunc("InferenceProfileHeader", func(ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) (out middleware.InitializeOutput, metadata middleware.Metadata, err error) {
req, ok := in.Parameters.(*smithyhttp.Request)
if ok {
req.Header.Set("X-Amzn-Bedrock-Inference-Profile-ARN", inferenceProfileArn)
}
return next.HandleInitialize(ctx, in)
}), middleware.Before)
})
})
}
// Invoke the model
var resp *bedrockruntime.InvokeModelOutput
if len(optFns) > 0 {
resp, err = a.client.InvokeModel(ctx, params, optFns...)
} else {
resp, err = a.client.InvokeModel(ctx, params)
}
if err != nil {
if strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "SignatureDoesNotMatch") || strings.Contains(err.Error(), "NoCredentialProviders") {
return "", fmt.Errorf("AWS credentials are invalid or missing. Please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or AWS config. Details: %v", err)
}
return "", err
}
// Parse the response
return a.model.Response.ParseResponse(resp.Body)
}
// GetName returns the name of the AmazonBedRockClient.

View File

@@ -0,0 +1,103 @@
package ai
import (
"context"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/bedrock"
"github.com/aws/aws-sdk-go-v2/service/bedrock/types"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Mock for Bedrock Management Client
type MockBedrockClient struct {
mock.Mock
}
func (m *MockBedrockClient) GetInferenceProfile(ctx context.Context, params *bedrock.GetInferenceProfileInput, optFns ...func(*bedrock.Options)) (*bedrock.GetInferenceProfileOutput, error) {
args := m.Called(ctx, params)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*bedrock.GetInferenceProfileOutput), args.Error(1)
}
// Mock for Bedrock Runtime Client
type MockBedrockRuntimeClient struct {
mock.Mock
}
func (m *MockBedrockRuntimeClient) InvokeModel(ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error) {
args := m.Called(ctx, params)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*bedrockruntime.InvokeModelOutput), args.Error(1)
}
// TestBedrockInferenceProfileARNWithMocks tests the inference profile ARN validation with mocks
func TestBedrockInferenceProfileARNWithMocks(t *testing.T) {
// Create test models
testModels := []bedrock_support.BedrockModel{
{
Name: "anthropic.claude-3-5-sonnet-20240620-v1:0",
Completion: &bedrock_support.CohereMessagesCompletion{},
Response: &bedrock_support.CohereMessagesResponse{},
Config: bedrock_support.BedrockModelConfig{
MaxTokens: 100,
Temperature: 0.5,
TopP: 0.9,
ModelName: "anthropic.claude-3-5-sonnet-20240620-v1:0",
},
},
}
// Create a client with test models
client := &AmazonBedRockClient{models: testModels}
// Create mock clients
mockMgmtClient := new(MockBedrockClient)
mockRuntimeClient := new(MockBedrockRuntimeClient)
// Inject mock clients into the AmazonBedRockClient
client.mgmtClient = mockMgmtClient
client.client = mockRuntimeClient
// Test with a valid inference profile ARN
inferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile"
// Setup mock response for GetInferenceProfile
mockMgmtClient.On("GetInferenceProfile", mock.Anything, &bedrock.GetInferenceProfileInput{
InferenceProfileIdentifier: aws.String("my-profile"),
}).Return(&bedrock.GetInferenceProfileOutput{
Models: []types.InferenceProfileModel{
{
ModelArn: aws.String("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0"),
},
},
}, nil)
// Configure the client with the inference profile ARN
config := AIProvider{
Model: inferenceProfileARN,
ProviderRegion: "us-east-1",
}
// Test the Configure method with the inference profile ARN
err := client.Configure(&config)
// Verify that the configuration was successful
assert.NoError(t, err)
assert.Equal(t, inferenceProfileARN, client.model.Config.ModelName)
// Verify that the mock was called
mockMgmtClient.AssertExpectations(t)
}

View File

@@ -0,0 +1,262 @@
package ai
import (
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/bedrock_support"
"github.com/stretchr/testify/assert"
)
// Test models for unit testing
var testModels = []bedrock_support.BedrockModel{
{
Name: "anthropic.claude-3-5-sonnet-20240620-v1:0",
Completion: &bedrock_support.CohereMessagesCompletion{},
Response: &bedrock_support.CohereMessagesResponse{},
Config: bedrock_support.BedrockModelConfig{
MaxTokens: 100,
Temperature: 0.5,
TopP: 0.9,
ModelName: "anthropic.claude-3-5-sonnet-20240620-v1:0",
},
},
{
Name: "anthropic.claude-3-5-sonnet-20241022-v2:0",
Completion: &bedrock_support.CohereCompletion{},
Response: &bedrock_support.CohereResponse{},
Config: bedrock_support.BedrockModelConfig{
MaxTokens: 100,
Temperature: 0.5,
TopP: 0.9,
ModelName: "anthropic.claude-3-5-sonnet-20241022-v2:0",
},
},
{
Name: "anthropic.claude-3-7-sonnet-20250219-v1:0",
Completion: &bedrock_support.CohereCompletion{},
Response: &bedrock_support.CohereResponse{},
Config: bedrock_support.BedrockModelConfig{
MaxTokens: 100,
Temperature: 0.5,
TopP: 0.9,
ModelName: "anthropic.claude-3-7-sonnet-20250219-v1:0",
},
},
}
func TestBedrockModelConfig(t *testing.T) {
client := &AmazonBedRockClient{models: testModels}
// Should return error for ARN input (no exact match)
_, err := client.getModelFromString("arn:aws:bedrock:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0")
assert.NotNil(t, err, "Should return error for ARN input")
}
func TestBedrockInvalidModel(t *testing.T) {
client := &AmazonBedRockClient{models: testModels}
// Should return error for invalid model name
_, err := client.getModelFromString("arn:aws:s3:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0")
assert.NotNil(t, err, "Should return error for invalid model name")
}
func TestBedrockInferenceProfileARN(t *testing.T) {
// Create a mock client with test models
client := &AmazonBedRockClient{models: testModels}
// Test with a valid inference profile ARN
inferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile"
config := AIProvider{
Model: inferenceProfileARN,
ProviderRegion: "us-east-1",
}
// This will fail in a real environment without mocks, but we're just testing the validation logic
err := client.Configure(&config)
// We expect an error since we can't actually call AWS in tests
assert.NotNil(t, err, "Error should not be nil without AWS mocks")
// Test with a valid application inference profile ARN
appInferenceProfileARN := "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile"
config = AIProvider{
Model: appInferenceProfileARN,
ProviderRegion: "us-east-1",
}
// This will fail in a real environment without mocks, but we're just testing the validation logic
err = client.Configure(&config)
// We expect an error since we can't actually call AWS in tests
assert.NotNil(t, err, "Error should not be nil without AWS mocks")
// Test with an invalid inference profile ARN format
invalidARN := "arn:aws:bedrock:us-east-1:123456789012:invalid-resource/my-profile"
config = AIProvider{
Model: invalidARN,
ProviderRegion: "us-east-1",
}
err = client.Configure(&config)
assert.NotNil(t, err, "Error should not be nil for invalid inference profile ARN format")
}
func TestBedrockGetCompletionInferenceProfile(t *testing.T) {
modelName := "arn:aws:bedrock:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0"
var inferenceModelModels = []bedrock_support.BedrockModel{
{
Name: "anthropic.claude-3-5-sonnet-20240620-v1:0",
Completion: &bedrock_support.CohereMessagesCompletion{},
Response: &bedrock_support.CohereMessagesResponse{},
Config: bedrock_support.BedrockModelConfig{
MaxTokens: 100,
Temperature: 0.5,
TopP: 0.9,
ModelName: modelName,
},
},
}
client := &AmazonBedRockClient{models: inferenceModelModels}
config := AIProvider{
Model: modelName,
}
err := client.Configure(&config)
assert.Nil(t, err, "Error should be nil")
assert.Equal(t, modelName, client.model.Config.ModelName, "Model name should match")
}
func TestGetModelFromString(t *testing.T) {
client := &AmazonBedRockClient{models: testModels}
tests := []struct {
name string
model string
wantModel string
wantErr bool
}{
{
name: "exact model name match",
model: "anthropic.claude-3-5-sonnet-20240620-v1:0",
wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0",
wantErr: false,
},
{
name: "partial model name match",
model: "claude-3-5-sonnet",
wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0",
wantErr: true,
},
{
name: "model name with different version",
model: "anthropic.claude-3-5-sonnet-20241022-v2:0",
wantModel: "anthropic.claude-3-5-sonnet-20241022-v2:0",
wantErr: false,
},
{
name: "non-existent model",
model: "non-existent-model",
wantModel: "",
wantErr: true,
},
{
name: "empty model name",
model: "",
wantModel: "",
wantErr: true,
},
{
name: "model name with extra spaces",
model: " anthropic.claude-3-5-sonnet-20240620-v1:0 ",
wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0",
wantErr: false,
},
{
name: "case insensitive match",
model: "ANTHROPIC.CLAUDE-3-5-SONNET-20240620-V1:0",
wantModel: "anthropic.claude-3-5-sonnet-20240620-v1:0",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotModel, err := client.getModelFromString(tt.model)
if (err != nil) != tt.wantErr {
t.Errorf("getModelFromString() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && gotModel.Name != tt.wantModel {
t.Errorf("getModelFromString() = %v, want %v", gotModel.Name, tt.wantModel)
}
})
}
}
// TestDefaultModels tests that the client works with default models
func TestDefaultModels(t *testing.T) {
client := &AmazonBedRockClient{}
// Configure should initialize default models
err := client.Configure(&AIProvider{
Model: "anthropic.claude-v2",
})
assert.NoError(t, err, "Configure should not return an error")
assert.NotNil(t, client.models, "Models should be initialized")
assert.NotEmpty(t, client.models, "Models should not be empty")
// Test finding a default model
model, err := client.getModelFromString("anthropic.claude-v2")
assert.NoError(t, err, "Should find the model")
assert.Equal(t, "anthropic.claude-v2", model.Name, "Should find the correct model")
}
func TestValidateInferenceProfileArn(t *testing.T) {
tests := []struct {
name string
arn string
valid bool
}{
{
name: "valid inference profile ARN",
arn: "arn:aws:bedrock:us-east-1:123456789012:inference-profile/my-profile",
valid: true,
},
{
name: "valid application inference profile ARN",
arn: "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-profile",
valid: true,
},
{
name: "invalid service in ARN",
arn: "arn:aws:s3:us-east-1:123456789012:inference-profile/my-profile",
valid: false,
},
{
name: "invalid resource type in ARN",
arn: "arn:aws:bedrock:us-east-1:123456789012:model/my-profile",
valid: false,
},
{
name: "malformed ARN",
arn: "arn:aws:bedrock:us-east-1:inference-profile/my-profile",
valid: false,
},
{
name: "not an ARN",
arn: "not-an-arn",
valid: false,
},
{
name: "empty string",
arn: "",
valid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := validateInferenceProfileArn(tt.arn)
assert.Equal(t, tt.valid, result, "validateInferenceProfileArn() result should match expected")
})
}
}

View File

@@ -0,0 +1,18 @@
package ai
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/bedrock"
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
)
// BedrockManagementAPI defines the interface for Bedrock management operations
type BedrockManagementAPI interface {
GetInferenceProfile(ctx context.Context, params *bedrock.GetInferenceProfileInput, optFns ...func(*bedrock.Options)) (*bedrock.GetInferenceProfileOutput, error)
}
// BedrockRuntimeAPI defines the interface for Bedrock runtime operations
type BedrockRuntimeAPI interface {
InvokeModel(ctx context.Context, params *bedrockruntime.InvokeModelInput, optFns ...func(*bedrockruntime.Options)) (*bedrockruntime.InvokeModelOutput, error)
}

View File

@@ -7,19 +7,6 @@ import (
"strings"
)
var SUPPPORTED_BEDROCK_MODELS = []string{
"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-lite-v1:0",
}
type ICompletion interface {
GetCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error)
}
@@ -89,18 +76,20 @@ type AmazonCompletion struct {
completion ICompletion
}
func isModelSupported(modelName string) bool {
for _, supportedModel := range SUPPPORTED_BEDROCK_MODELS {
if modelName == supportedModel {
// 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 !isModelSupported(modelConfig.ModelName) {
return nil, fmt.Errorf("model %s is not supported", modelConfig.ModelName)
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)
@@ -123,7 +112,6 @@ func (a *AmazonCompletion) GetDefaultCompletion(ctx context.Context, prompt stri
return []byte{}, err
}
return body, nil
}
func (a *AmazonCompletion) GetNovaCompletion(ctx context.Context, prompt string, modelConfig BedrockModelConfig) ([]byte, error) {

View File

@@ -158,22 +158,25 @@ func TestAmazonCompletion_GetCompletion_Default(t *testing.T) {
assert.Equal(t, 0.7, textConfig["topP"])
}
func TestAmazonCompletion_GetCompletion_UnsupportedModel(t *testing.T) {
func TestAmazonCompletion_GetCompletion_Inference_Profile(t *testing.T) {
completion := &AmazonCompletion{}
modelConfig := BedrockModelConfig{
MaxTokens: 200,
Temperature: 0.5,
TopP: 0.7,
ModelName: "unsupported-model",
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.Error(t, err)
assert.Contains(t, err.Error(), "model unsupported-model is not supported")
assert.NoError(t, err)
}
func Test_isModelSupported(t *testing.T) {
assert.True(t, isModelSupported("anthropic.claude-v2"))
assert.False(t, isModelSupported("unsupported-model"))
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))
}

87
pkg/ai/factory.go Normal file
View File

@@ -0,0 +1,87 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ai
import (
"github.com/spf13/viper"
)
// AIClientFactory is an interface for creating AI clients
type AIClientFactory interface {
NewClient(provider string) IAI
}
// DefaultAIClientFactory is the default implementation of AIClientFactory
type DefaultAIClientFactory struct{}
// NewClient creates a new AI client using the default implementation
func (f *DefaultAIClientFactory) NewClient(provider string) IAI {
return NewClient(provider)
}
// ConfigProvider is an interface for accessing configuration
type ConfigProvider interface {
UnmarshalKey(key string, rawVal interface{}) error
}
// ViperConfigProvider is the default implementation of ConfigProvider using Viper
type ViperConfigProvider struct{}
// UnmarshalKey unmarshals a key from the configuration using Viper
func (p *ViperConfigProvider) UnmarshalKey(key string, rawVal interface{}) error {
return viper.UnmarshalKey(key, rawVal)
}
// Default instances to be used
var (
DefaultClientFactory = &DefaultAIClientFactory{}
DefaultConfigProvider = &ViperConfigProvider{}
)
// For testing - these variables can be overridden in tests
var (
testAIClientFactory AIClientFactory = nil
testConfigProvider ConfigProvider = nil
)
// GetAIClientFactory returns the test factory if set, otherwise the default
func GetAIClientFactory() AIClientFactory {
if testAIClientFactory != nil {
return testAIClientFactory
}
return DefaultClientFactory
}
// GetConfigProvider returns the test provider if set, otherwise the default
func GetConfigProvider() ConfigProvider {
if testConfigProvider != nil {
return testConfigProvider
}
return DefaultConfigProvider
}
// For testing - set the test implementations
func SetTestAIClientFactory(factory AIClientFactory) {
testAIClientFactory = factory
}
func SetTestConfigProvider(provider ConfigProvider) {
testConfigProvider = provider
}
// Reset test implementations
func ResetTestImplementations() {
testAIClientFactory = nil
testConfigProvider = nil
}

View File

@@ -61,10 +61,22 @@ var VERTEXAI_SUPPORTED_REGION = []string{
}
const (
ModelGeminiProV1 = "gemini-1.0-pro-001"
ModelGeminiProV1 = "gemini-1.0-pro-001" // Retired Model
ModelGeminiProV2_5 = "gemini-2.5-pro" // Latest Stable Model
ModelGeminiFlashV2_5 = "gemini-2.5-flash" // Latest Stable Model
ModelGeminiFlashV2 = "gemini-2.0-flash" // Latest Stable Model
ModelGeminiFlashLiteV2 = "gemini-2.0-flash-lite" // Latest Stable Model
ModelGeminiProV1_5 = "gemini-1.5-pro-002*" // Legacy Stable Model
ModelGeminiFlashV1_5 = "gemini-1.5-flash-002*" // Legacy Stable Model
)
var VERTEXAI_MODELS = []string{
ModelGeminiProV2_5,
ModelGeminiFlashV2_5,
ModelGeminiFlashV2,
ModelGeminiFlashLiteV2,
ModelGeminiProV1_5,
ModelGeminiFlashV1_5,
ModelGeminiProV1,
}

View File

@@ -16,21 +16,32 @@ package ai
import (
"context"
"errors"
"fmt"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/generativeai"
"github.com/oracle/oci-go-sdk/v65/generativeaiinference"
"strings"
"reflect"
)
const ociClientName = "oci"
type ociModelVendor string
const (
vendorCohere = "cohere"
vendorMeta = "meta"
)
type OCIGenAIClient struct {
nopCloser
client *generativeaiinference.GenerativeAiInferenceClient
model string
model *generativeai.Model
modelID string
compartmentId string
temperature float32
topP float32
topK int32
maxTokens int
}
@@ -40,9 +51,10 @@ func (c *OCIGenAIClient) GetName() string {
func (c *OCIGenAIClient) Configure(config IAIConfig) error {
config.GetEndpointName()
c.model = config.GetModel()
c.modelID = config.GetModel()
c.temperature = config.GetTemperature()
c.topP = config.GetTopP()
c.topK = config.GetTopK()
c.maxTokens = config.GetMaxTokens()
c.compartmentId = config.GetCompartmentId()
provider := common.DefaultConfigProvider()
@@ -51,47 +63,123 @@ func (c *OCIGenAIClient) Configure(config IAIConfig) error {
return err
}
c.client = &client
model, err := c.getModel(provider)
if err != nil {
return err
}
c.model = model
return nil
}
func (c *OCIGenAIClient) GetCompletion(ctx context.Context, prompt string) (string, error) {
generateTextRequest := c.newGenerateTextRequest(prompt)
generateTextResponse, err := c.client.GenerateText(ctx, generateTextRequest)
request := c.newChatRequest(prompt)
response, err := c.client.Chat(ctx, request)
if err != nil {
return "", err
}
return extractGeneratedText(generateTextResponse.InferenceResponse)
if err != nil {
return "", err
}
return extractGeneratedText(response.ChatResponse)
}
func (c *OCIGenAIClient) newGenerateTextRequest(prompt string) generativeaiinference.GenerateTextRequest {
temperatureF64 := float64(c.temperature)
topPF64 := float64(c.topP)
return generativeaiinference.GenerateTextRequest{
GenerateTextDetails: generativeaiinference.GenerateTextDetails{
func (c *OCIGenAIClient) newChatRequest(prompt string) generativeaiinference.ChatRequest {
return generativeaiinference.ChatRequest{
ChatDetails: generativeaiinference.ChatDetails{
CompartmentId: &c.compartmentId,
ServingMode: generativeaiinference.OnDemandServingMode{
ModelId: &c.model,
},
InferenceRequest: generativeaiinference.CohereLlmInferenceRequest{
Prompt: &prompt,
MaxTokens: &c.maxTokens,
Temperature: &temperatureF64,
TopP: &topPF64,
},
ServingMode: c.getServingMode(),
ChatRequest: c.getChatModelRequest(prompt),
},
}
}
func extractGeneratedText(llmInferenceResponse generativeaiinference.LlmInferenceResponse) (string, error) {
response, ok := llmInferenceResponse.(generativeaiinference.CohereLlmInferenceResponse)
if !ok {
return "", errors.New("failed to extract generated text from backed response")
func (c *OCIGenAIClient) getChatModelRequest(prompt string) generativeaiinference.BaseChatRequest {
temperatureF64 := float64(c.temperature)
topPF64 := float64(c.topP)
topK := int(c.topK)
switch c.getVendor() {
case vendorMeta:
messages := []generativeaiinference.Message{
generativeaiinference.UserMessage{
Content: []generativeaiinference.ChatContent{
generativeaiinference.TextContent{
Text: &prompt,
},
},
},
}
// 0 is invalid for Meta vendor type, instead use -1 to disable topK sampling.
if topK == 0 {
topK = -1
}
return generativeaiinference.GenericChatRequest{
Messages: messages,
TopK: &topK,
TopP: &topPF64,
Temperature: &temperatureF64,
MaxTokens: &c.maxTokens,
}
default: // Default to cohere
return generativeaiinference.CohereChatRequest{
Message: &prompt,
MaxTokens: &c.maxTokens,
Temperature: &temperatureF64,
TopK: &topK,
TopP: &topPF64,
}
}
sb := strings.Builder{}
for _, text := range response.GeneratedTexts {
if text.Text != nil {
sb.WriteString(*text.Text)
}
func extractGeneratedText(llmInferenceResponse generativeaiinference.BaseChatResponse) (string, error) {
switch response := llmInferenceResponse.(type) {
case generativeaiinference.GenericChatResponse:
if len(response.Choices) > 0 && len(response.Choices[0].Message.GetContent()) > 0 {
if content, ok := response.Choices[0].Message.GetContent()[0].(generativeaiinference.TextContent); ok {
return *content.Text, nil
}
}
return "", errors.New("no text found in oci response")
case generativeaiinference.CohereChatResponse:
return *response.Text, nil
default:
return "", fmt.Errorf("unknown oci response type: %s", reflect.TypeOf(llmInferenceResponse).Name())
}
}
func (c *OCIGenAIClient) getServingMode() generativeaiinference.ServingMode {
if c.isBaseModel() {
return generativeaiinference.OnDemandServingMode{
ModelId: &c.modelID,
}
}
return sb.String(), nil
return generativeaiinference.DedicatedServingMode{
EndpointId: &c.modelID,
}
}
func (c *OCIGenAIClient) getModel(provider common.ConfigurationProvider) (*generativeai.Model, error) {
client, err := generativeai.NewGenerativeAiClientWithConfigurationProvider(provider)
if err != nil {
return nil, err
}
response, err := client.GetModel(context.Background(), generativeai.GetModelRequest{
ModelId: &c.modelID,
})
if err != nil {
return nil, err
}
return &response.Model, nil
}
func (c *OCIGenAIClient) isBaseModel() bool {
return c.model != nil && c.model.Type == generativeai.ModelTypeBase
}
func (c *OCIGenAIClient) getVendor() ociModelVendor {
if c.model == nil || c.model.Vendor == nil {
return ""
}
return ociModelVendor(*c.model.Vendor)
}

View File

@@ -18,6 +18,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"reflect"
"strings"
"sync"
"time"
@@ -89,19 +90,35 @@ func NewAnalysis(
// Get kubernetes client from viper.
kubecontext := viper.GetString("kubecontext")
kubeconfig := viper.GetString("kubeconfig")
verbose := viper.GetBool("verbose")
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
if verbose {
fmt.Println("Debug: Checking kubernetes client initialization.")
}
if err != nil {
return nil, fmt.Errorf("initialising kubernetes client: %w", err)
}
if verbose {
fmt.Printf("Debug: Kubernetes client initialized, server=%s.\n", client.Config.Host)
}
// Load remote cache if it is configured.
cache, err := cache.GetCacheConfiguration()
if verbose {
fmt.Println("Debug: Checking cache configuration.")
}
if err != nil {
return nil, err
}
if verbose {
fmt.Printf("Debug: Cache configuration loaded, type=%s.\n", cache.GetName())
}
if noCache {
cache.DisableCache()
if verbose {
fmt.Println("Debug: Cache disabled.")
}
}
a := &Analysis{
@@ -117,12 +134,31 @@ func NewAnalysis(
WithDoc: withDoc,
WithStats: withStats,
}
if verbose {
fmt.Print("Debug: Analysis configuration loaded, ")
fmt.Printf("filters=%v, language=%s, ", filters, language)
if namespace == "" {
fmt.Printf("namespace=none, ")
} else {
fmt.Printf("namespace=%s, ", namespace)
}
if labelSelector == "" {
fmt.Printf("labelSelector=none, ")
} else {
fmt.Printf("labelSelector=%s, ", labelSelector)
}
fmt.Printf("explain=%t, maxConcurrency=%d, ", explain, maxConcurrency)
fmt.Printf("withDoc=%t, withStats=%t.\n", withDoc, withStats)
}
if !explain {
// Return early if AI use was not requested.
return a, nil
}
var configAI ai.AIConfiguration
if verbose {
fmt.Println("Debug: Checking AI configuration.")
}
if err := viper.UnmarshalKey("ai", &configAI); err != nil {
return nil, err
}
@@ -135,10 +171,16 @@ func NewAnalysis(
// Hence, use the default provider only if the backend is not specified by the user.
if configAI.DefaultProvider != "" && backend == "" {
backend = configAI.DefaultProvider
if verbose {
fmt.Printf("Debug: Using default AI provider %s.\n", backend)
}
}
if backend == "" {
backend = "openai"
if verbose {
fmt.Printf("Debug: Using default AI provider %s.\n", backend)
}
}
var aiProvider ai.AIProvider
@@ -153,12 +195,23 @@ func NewAnalysis(
return nil, fmt.Errorf("AI provider %s not specified in configuration. Please run k8sgpt auth", backend)
}
if verbose {
fmt.Printf("Debug: AI configuration loaded, provider=%s, ", backend)
fmt.Printf("baseUrl=%s, model=%s.\n", aiProvider.BaseURL, aiProvider.Model)
}
aiClient := ai.NewClient(aiProvider.Name)
customHeaders := util.NewHeaders(httpHeaders)
aiProvider.CustomHeaders = customHeaders
if verbose {
fmt.Println("Debug: Checking AI client initialization.")
}
if err := aiClient.Configure(&aiProvider); err != nil {
return nil, err
}
if verbose {
fmt.Println("Debug: AI client initialized.")
}
a.AIClient = aiClient
a.AnalysisAIProvider = aiProvider.Name
return a, nil
@@ -182,6 +235,18 @@ func (a *Analysis) RunCustomAnalysis() {
semaphore := make(chan struct{}, a.MaxConcurrency)
var wg sync.WaitGroup
var mutex sync.Mutex
verbose := viper.GetBool("verbose")
if verbose {
if len(customAnalyzers) == 0 {
fmt.Println("Debug: No custom analyzers found.")
} else {
cAnalyzerNames := make([]string, len(customAnalyzers))
for i, cAnalyzer := range customAnalyzers {
cAnalyzerNames[i] = cAnalyzer.Name
}
fmt.Printf("Debug: Found custom analyzers %v.\n", cAnalyzerNames)
}
}
for _, cAnalyzer := range customAnalyzers {
wg.Add(1)
semaphore <- struct{}{}
@@ -194,6 +259,9 @@ func (a *Analysis) RunCustomAnalysis() {
mutex.Unlock()
return
}
if verbose {
fmt.Printf("Debug: %s launched.\n", cAnalyzer.Name)
}
result, err := canClient.Run()
if result.Kind == "" {
@@ -206,10 +274,16 @@ func (a *Analysis) RunCustomAnalysis() {
mutex.Lock()
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", cAnalyzer.Name, err))
mutex.Unlock()
if verbose {
fmt.Printf("Debug: %s completed with errors.\n", cAnalyzer.Name)
}
} else {
mutex.Lock()
a.Results = append(a.Results, result)
mutex.Unlock()
if verbose {
fmt.Printf("Debug: %s completed without errors.\n", cAnalyzer.Name)
}
}
<-semaphore
}(cAnalyzer, &wg, semaphore)
@@ -219,6 +293,7 @@ func (a *Analysis) RunCustomAnalysis() {
func (a *Analysis) RunAnalysis() {
activeFilters := viper.GetStringSlice("active_filters")
verbose := viper.GetBool("verbose")
coreAnalyzerMap, analyzerMap := analyzer.GetAnalyzerMap()
@@ -227,7 +302,13 @@ func (a *Analysis) RunAnalysis() {
if a.WithDoc {
var openApiErr error
if verbose {
fmt.Println("Debug: Fetching Kubernetes docs.")
}
openapiSchema, openApiErr = a.Client.Client.Discovery().OpenAPISchema()
if verbose {
fmt.Println("Debug: Checking Kubernetes docs.")
}
if openApiErr != nil {
a.Errors = append(a.Errors, fmt.Sprintf("[KubernetesDoc] %s", openApiErr))
}
@@ -242,11 +323,23 @@ func (a *Analysis) RunAnalysis() {
OpenapiSchema: openapiSchema,
}
semaphore := make(chan struct{}, a.MaxConcurrency)
// Set a reasonable maximum for concurrency to prevent excessive memory allocation
const maxAllowedConcurrency = 100
concurrency := a.MaxConcurrency
if concurrency <= 0 {
concurrency = 10 // Default value if not set
} else if concurrency > maxAllowedConcurrency {
concurrency = maxAllowedConcurrency // Cap at a reasonable maximum
}
semaphore := make(chan struct{}, concurrency)
var wg sync.WaitGroup
var mutex sync.Mutex
// if there are no filters selected and no active_filters then run coreAnalyzer
if len(a.Filters) == 0 && len(activeFilters) == 0 {
if verbose {
fmt.Println("Debug: No filters selected and no active filters found, run all core analyzers.")
}
for name, analyzer := range coreAnalyzerMap {
wg.Add(1)
semaphore <- struct{}{}
@@ -258,6 +351,9 @@ func (a *Analysis) RunAnalysis() {
}
// if the filters flag is specified
if len(a.Filters) != 0 {
if verbose {
fmt.Printf("Debug: Filter flags %v specified, run selected core analyzers.\n", a.Filters)
}
for _, filter := range a.Filters {
if analyzer, ok := analyzerMap[filter]; ok {
semaphore <- struct{}{}
@@ -272,6 +368,9 @@ func (a *Analysis) RunAnalysis() {
}
// use active_filters
if len(activeFilters) > 0 && verbose {
fmt.Printf("Debug: Found active filters %v, run selected core analyzers.\n", activeFilters)
}
for _, filter := range activeFilters {
if analyzer, ok := analyzerMap[filter]; ok {
semaphore <- struct{}{}
@@ -294,6 +393,10 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana
}
// Run the analyzer
verbose := viper.GetBool("verbose")
if verbose {
fmt.Printf("Debug: %s launched.\n", reflect.TypeOf(analyzer).Name())
}
results, err := analyzer.Analyze(analyzerConfig)
if err != nil {
fmt.Println(err)
@@ -315,11 +418,17 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana
a.Stats = append(a.Stats, stat)
}
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
if verbose {
fmt.Printf("Debug: %s completed with errors.\n", reflect.TypeOf(analyzer).Name())
}
} else {
if a.WithStats {
a.Stats = append(a.Stats, stat)
}
a.Results = append(a.Results, results...)
if verbose {
fmt.Printf("Debug: %s completed without errors.\n", reflect.TypeOf(analyzer).Name())
}
}
<-semaphore
}
@@ -329,6 +438,11 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
return nil
}
verbose := viper.GetBool("verbose")
if verbose {
fmt.Println("Debug: Generating AI analysis.")
}
var bar *progressbar.ProgressBar
if output != "json" {
bar = progressbar.Default(int64(len(a.Results)))
@@ -337,6 +451,10 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
for index, analysis := range a.Results {
var texts []string
if bar != nil && verbose {
bar.Describe(fmt.Sprintf("Analyzing %s", analysis.Kind))
}
for _, failure := range analysis.Error {
if anonymize {
for _, s := range failure.Sensitive {

View File

@@ -17,13 +17,17 @@ import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
"github.com/agiledragon/gomonkey/v2"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
"github.com/magiconair/properties/assert"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"
@@ -31,9 +35,15 @@ import (
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/rest"
)
// sub-function
// helper function: get type name of an analyzer
func getTypeName(i interface{}) string {
return reflect.TypeOf(i).Name()
}
// helper function: run analysis with filter
func analysis_RunAnalysisFilterTester(t *testing.T, filterFlag string) []common.Result {
clientset := fake.NewSimpleClientset(
&v1.Pod{
@@ -404,3 +414,252 @@ func TestGetAIResultForSanitizedFailures(t *testing.T) {
})
}
}
// Test: Verbose output in NewAnalysis with explain=false
func TestVerbose_NewAnalysisWithoutExplain(t *testing.T) {
// Set viper config.
viper.Set("verbose", true)
viper.Set("kubecontext", "dummy")
viper.Set("kubeconfig", "dummy")
// Patch kubernetes.NewClient to return a dummy client.
patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) {
return &kubernetes.Client{
Config: &rest.Config{Host: "fake-server"},
}, nil
})
defer patches.Reset()
output := util.CaptureOutput(func() {
a, err := NewAnalysis(
"", "english", []string{"Pod"}, "default", "", true,
false, // explain
10, false, false, []string{}, false,
)
require.NoError(t, err)
a.Close()
})
expectedOutputs := []string{
"Debug: Checking kubernetes client initialization.",
"Debug: Kubernetes client initialized, server=fake-server.",
"Debug: Checking cache configuration.",
"Debug: Cache configuration loaded, type=file.",
"Debug: Cache disabled.",
"Debug: Analysis configuration loaded, filters=[Pod], language=english, namespace=default, labelSelector=none, explain=false, maxConcurrency=10, withDoc=false, withStats=false.",
}
for _, expected := range expectedOutputs {
if !util.Contains(output, expected) {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
}
}
}
// Test: Verbose output in NewAnalysis with explain=true
func TestVerbose_NewAnalysisWithExplain(t *testing.T) {
// Set viper config.
viper.Set("verbose", true)
viper.Set("kubecontext", "dummy")
viper.Set("kubeconfig", "dummy")
// Set a dummy AI configuration.
dummyAIConfig := map[string]interface{}{
"defaultProvider": "dummy",
"providers": []map[string]interface{}{
{
"name": "dummy",
"baseUrl": "http://dummy",
"model": "dummy-model",
"customHeaders": map[string]string{},
},
},
}
viper.Set("ai", dummyAIConfig)
// Patch kubernetes.NewClient to return a dummy client.
patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) {
return &kubernetes.Client{
Config: &rest.Config{Host: "fake-server"},
}, nil
})
defer patches.Reset()
// Patch ai.NewClient to return a NoOp client.
patches2 := gomonkey.ApplyFunc(ai.NewClient, func(name string) ai.IAI {
return &ai.NoOpAIClient{}
})
defer patches2.Reset()
output := util.CaptureOutput(func() {
a, err := NewAnalysis(
"", "english", []string{"Pod"}, "default", "", true,
true, // explain
10, false, false, []string{}, false,
)
require.NoError(t, err)
a.Close()
})
expectedOutputs := []string{
"Debug: Checking AI configuration.",
"Debug: Using default AI provider dummy.",
"Debug: AI configuration loaded, provider=dummy, baseUrl=http://dummy, model=dummy-model.",
"Debug: Checking AI client initialization.",
"Debug: AI client initialized.",
}
for _, expected := range expectedOutputs {
if !util.Contains(output, expected) {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
}
}
}
// Test: Verbose output in RunAnalysis with filter flag
func TestVerbose_RunAnalysisWithFilter(t *testing.T) {
viper.Set("verbose", true)
// Run analysis with a filter flag ("Pod") to trigger debug output.
output := util.CaptureOutput(func() {
_ = analysis_RunAnalysisFilterTester(t, "Pod")
})
expectedOutputs := []string{
"Debug: Filter flags [Pod] specified, run selected core analyzers.",
"Debug: PodAnalyzer launched.",
"Debug: PodAnalyzer completed without errors.",
}
for _, expected := range expectedOutputs {
if !util.Contains(output, expected) {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
}
}
}
// Test: Verbose output in RunAnalysis with active filter
func TestVerbose_RunAnalysisWithActiveFilter(t *testing.T) {
viper.Set("verbose", true)
viper.SetDefault("active_filters", "Ingress")
output := util.CaptureOutput(func() {
_ = analysis_RunAnalysisFilterTester(t, "")
})
expectedOutputs := []string{
"Debug: Found active filters [Ingress], run selected core analyzers.",
"Debug: IngressAnalyzer launched.",
"Debug: IngressAnalyzer completed without errors.",
}
for _, expected := range expectedOutputs {
if !util.Contains(output, expected) {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
}
}
}
// Test: Verbose output in RunAnalysis without any filter (run all core analyzers)
func TestVerbose_RunAnalysisWithoutFilter(t *testing.T) {
viper.Set("verbose", true)
// Clear filter flag and active_filters to run all core analyzers.
viper.SetDefault("active_filters", []string{})
output := util.CaptureOutput(func() {
_ = analysis_RunAnalysisFilterTester(t, "")
})
// Check for debug message indicating no filters.
expectedNoFilter := "Debug: No filters selected and no active filters found, run all core analyzers."
if !util.Contains(output, expectedNoFilter) {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedNoFilter, output)
}
// Get all core analyzers from analyzer.GetAnalyzerMap()
coreAnalyzerMap, _ := analyzer.GetAnalyzerMap()
for _, analyzerInstance := range coreAnalyzerMap {
analyzerType := getTypeName(analyzerInstance)
expectedLaunched := fmt.Sprintf("Debug: %s launched.", analyzerType)
expectedCompleted := fmt.Sprintf("Debug: %s completed without errors.", analyzerType)
if !util.Contains(output, expectedLaunched) {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedLaunched, output)
}
if !util.Contains(output, expectedCompleted) {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedCompleted, output)
}
}
}
// Test: Verbose output in RunCustomAnalysis without custom analyzer
func TestVerbose_RunCustomAnalysisWithoutCustomAnalyzer(t *testing.T) {
viper.Set("verbose", true)
// Set custom_analyzers to empty array to trigger "No custom analyzers" debug message.
viper.Set("custom_analyzers", []interface{}{})
analysisObj := &Analysis{
MaxConcurrency: 1,
}
output := util.CaptureOutput(func() {
analysisObj.RunCustomAnalysis()
})
expected := "Debug: No custom analyzers found."
if !util.Contains(output, "Debug: No custom analyzers found.") {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
}
}
// Test: Verbose output in RunCustomAnalysis with custom analyzer
func TestVerbose_RunCustomAnalysisWithCustomAnalyzer(t *testing.T) {
viper.Set("verbose", true)
// Set custom_analyzers with one custom analyzer using "fake" connection.
viper.Set("custom_analyzers", []map[string]interface{}{
{
"name": "TestCustomAnalyzer",
"connection": map[string]interface{}{"url": "127.0.0.1", "port": "2333"},
},
})
analysisObj := &Analysis{
MaxConcurrency: 1,
}
output := util.CaptureOutput(func() {
analysisObj.RunCustomAnalysis()
})
assert.Equal(t, 1, len(analysisObj.Errors)) // connection error
expectedOutputs := []string{
"Debug: Found custom analyzers [TestCustomAnalyzer].",
"Debug: TestCustomAnalyzer launched.",
"Debug: TestCustomAnalyzer completed with errors.",
}
for _, expected := range expectedOutputs {
if !util.Contains(output, expected) {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
}
}
}
// Test: Verbose output in GetAIResults
func TestVerbose_GetAIResults(t *testing.T) {
viper.Set("verbose", true)
disabledCache := cache.New("disabled-cache")
disabledCache.DisableCache()
aiClient := &ai.NoOpAIClient{}
analysisObj := Analysis{
AIClient: aiClient,
Cache: disabledCache,
Results: []common.Result{
{
Kind: "Deployment",
Name: "test-deployment",
Error: []common.Failure{{Text: "test-problem", Sensitive: []common.Sensitive{}}},
Details: "test-solution",
ParentObject: "parent-resource",
},
},
Namespace: "default",
}
output := util.CaptureOutput(func() {
_ = analysisObj.GetAIResults("json", false)
})
expected := "Debug: Generating AI analysis."
if !util.Contains(output, expected) {
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
}
}

View File

@@ -39,20 +39,31 @@ var coreAnalyzerMap = map[string]common.IAnalyzer{
"Service": ServiceAnalyzer{},
"Ingress": IngressAnalyzer{},
"StatefulSet": StatefulSetAnalyzer{},
"Job": JobAnalyzer{},
"CronJob": CronJobAnalyzer{},
"Node": NodeAnalyzer{},
"ValidatingWebhookConfiguration": ValidatingWebhookAnalyzer{},
"MutatingWebhookConfiguration": MutatingWebhookAnalyzer{},
"ConfigMap": ConfigMapAnalyzer{},
}
var additionalAnalyzerMap = map[string]common.IAnalyzer{
"HorizontalPodAutoScaler": HpaAnalyzer{},
"HorizontalPodAutoscaler": HpaAnalyzer{},
"PodDisruptionBudget": PdbAnalyzer{},
"NetworkPolicy": NetworkPolicyAnalyzer{},
"Log": LogAnalyzer{},
"GatewayClass": GatewayClassAnalyzer{},
"Gateway": GatewayAnalyzer{},
"HTTPRoute": HTTPRouteAnalyzer{},
"Storage": StorageAnalyzer{},
"Security": SecurityAnalyzer{},
"ClusterCatalog": ClusterCatalogAnalyzer{},
"ClusterExtension": ClusterExtensionAnalyzer{},
"ClusterServiceVersion": ClusterServiceVersionAnalyzer{},
"Subscription": SubscriptionAnalyzer{},
"InstallPlan": InstallPlanAnalyzer{},
"CatalogSource": CatalogSourceAnalyzer{},
"OperatorGroup": OperatorGroupAnalyzer{},
}
func ListFilters() ([]string, []string, []string) {

View File

@@ -0,0 +1,53 @@
package analyzer
import (
"fmt"
"strings"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type CatalogSourceAnalyzer struct{}
var catSrcGVR = schema.GroupVersionResource{
Group: "operators.coreos.com",
Version: "v1alpha1",
Resource: "catalogsources",
}
func (CatalogSourceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "CatalogSource"
if a.Client.GetDynamicClient() == nil {
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
}
list, err := a.Client.GetDynamicClient().
Resource(catSrcGVR).Namespace(metav1.NamespaceAll).
List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
var results []common.Result
for _, item := range list.Items {
ns, name := item.GetNamespace(), item.GetName()
state, _, _ := unstructured.NestedString(item.Object, "status", "connectionState", "lastObservedState")
addr, _, _ := unstructured.NestedString(item.Object, "status", "connectionState", "address")
// Only report if state is present and not READY
if state != "" && strings.ToUpper(state) != "READY" {
results = append(results, common.Result{
Kind: kind,
Name: ns + "/" + name,
Error: []common.Failure{{
Text: fmt.Sprintf("connectionState=%s (address=%s)", state, addr),
}},
})
}
}
return results, nil
}

View File

@@ -0,0 +1,107 @@
package analyzer
import (
"context"
"strings"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
)
func TestCatalogSourceAnalyzer_UnhealthyState_ReturnsResult(t *testing.T) {
cs := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1alpha1",
"kind": "CatalogSource",
"metadata": map[string]any{
"name": "broken-operators-external",
"namespace": "openshift-marketplace",
},
"status": map[string]any{
"connectionState": map[string]any{
"lastObservedState": "TRANSIENT_FAILURE",
"address": "not-a-real-host.invalid:50051",
},
},
},
}
listKinds := map[schema.GroupVersionResource]string{
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "catalogsources"}: "CatalogSourceList",
}
scheme := runtime.NewScheme()
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, cs)
a := common.Analyzer{
Context: context.TODO(),
Client: &kubernetes.Client{DynamicClient: dc},
}
res, err := (CatalogSourceAnalyzer{}).Analyze(a)
if err != nil {
t.Fatalf("Analyze error: %v", err)
}
if len(res) != 1 {
t.Fatalf("expected 1 result, got %d", len(res))
}
if res[0].Kind != "CatalogSource" || !strings.Contains(res[0].Name, "openshift-marketplace/broken-operators-external") {
t.Fatalf("unexpected result: %#v", res[0])
}
if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "TRANSIENT_FAILURE") {
t.Fatalf("expected TRANSIENT_FAILURE in message, got %#v", res[0].Error)
}
}
func TestCatalogSourceAnalyzer_HealthyOrNoState_Ignored(t *testing.T) {
// One READY (healthy), one with no status at all: both should be ignored.
ready := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1alpha1",
"kind": "CatalogSource",
"metadata": map[string]any{
"name": "ready-operators",
"namespace": "openshift-marketplace",
},
"status": map[string]any{
"connectionState": map[string]any{
"lastObservedState": "READY",
"address": "somewhere",
},
},
},
}
nostate := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1alpha1",
"kind": "CatalogSource",
"metadata": map[string]any{
"name": "no-status-operators",
"namespace": "openshift-marketplace",
},
},
}
listKinds := map[schema.GroupVersionResource]string{
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "catalogsources"}: "CatalogSourceList",
}
scheme := runtime.NewScheme()
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ready, nostate)
a := common.Analyzer{
Context: context.TODO(),
Client: &kubernetes.Client{DynamicClient: dc},
}
res, err := (CatalogSourceAnalyzer{}).Analyze(a)
if err != nil {
t.Fatalf("Analyze error: %v", err)
}
if len(res) != 0 {
t.Fatalf("expected 0 results (healthy/nostate ignored), got %d", len(res))
}
}

View File

@@ -0,0 +1,161 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"fmt"
"regexp"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type ClusterCatalogAnalyzer struct{}
func (ClusterCatalogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "ClusterCatalog"
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
"analyzer_name": kind,
})
var clusterCatalogGVR = schema.GroupVersionResource{
Group: "olm.operatorframework.io",
Version: "v1",
Resource: "clustercatalogs",
}
if a.Client == nil {
return nil, fmt.Errorf("client is nil in ClusterCatalogAnalyzer")
}
if a.Client.GetDynamicClient() == nil {
return nil, fmt.Errorf("dynamic client is nil in ClusterCatalogAnalyzer")
}
list, err := a.Client.GetDynamicClient().Resource(clusterCatalogGVR).Namespace("").List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
var preAnalysis = map[string]common.PreAnalysis{}
for _, item := range list.Items {
var failures []common.Failure
catalog, err := ConvertToClusterCatalog(&item)
if err != nil {
continue
}
fmt.Printf("ClusterCatalog: %s | Source: %s\n", catalog.Name, catalog.Spec.Source.Image.Ref)
failures, err = ValidateClusterCatalog(failures, catalog)
if err != nil {
continue
}
if len(failures) > 0 {
preAnalysis[catalog.Name] = common.PreAnalysis{
Catalog: *catalog,
FailureDetails: failures,
}
AnalyzerErrorsMetric.WithLabelValues(kind, catalog.Name, "").Set(float64(len(failures)))
}
}
for key, value := range preAnalysis {
var currentAnalysis = common.Result{
Kind: kind,
Name: key,
Error: value.FailureDetails,
}
parent, found := util.GetParent(a.Client, value.Node.ObjectMeta)
if found {
currentAnalysis.ParentObject = parent
}
a.Results = append(a.Results, currentAnalysis)
}
return a.Results, err
}
func ConvertToClusterCatalog(u *unstructured.Unstructured) (*common.ClusterCatalog, error) {
var cc common.ClusterCatalog
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &cc)
if err != nil {
return nil, fmt.Errorf("failed to convert to ClusterCatalog: %w", err)
}
return &cc, nil
}
func addCatalogConditionFailure(failures []common.Failure, catalogName string, catalogCondition metav1.Condition) []common.Failure {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("OLMv1 ClusterCatalog: %s has condition of type %s, reason %s: %s", catalogName, catalogCondition.Type, catalogCondition.Reason, catalogCondition.Message),
Sensitive: []common.Sensitive{
{
Unmasked: catalogName,
Masked: util.MaskString(catalogName),
},
},
})
return failures
}
func addCatalogFailure(failures []common.Failure, catalogName string, err error) []common.Failure {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("%s has error: %s", catalogName, err.Error()),
Sensitive: []common.Sensitive{
{
Unmasked: catalogName,
Masked: util.MaskString(catalogName),
},
},
})
return failures
}
func ValidateClusterCatalog(failures []common.Failure, catalog *common.ClusterCatalog) ([]common.Failure, error) {
if !isValidImageRef(catalog.Spec.Source.Image.Ref) {
failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("invalid image ref format in spec.source.image.ref: %s", catalog.Spec.Source.Image.Ref))
}
// Check status.resolvedSource.image.ref ends with @sha256:...
if catalog.Status.ResolvedSource != nil {
if catalog.Status.ResolvedSource.Image.Ref == "" {
failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("missing status.resolvedSource.image.ref"))
}
if !regexp.MustCompile(`@sha256:[a-f0-9]{64}$`).MatchString(catalog.Status.ResolvedSource.Image.Ref) {
failures = addCatalogFailure(failures, catalog.Name, fmt.Errorf("status.resolvedSource.image.ref must end with @sha256:<digest>"))
}
}
for _, condition := range catalog.Status.Conditions {
if condition.Status != "True" && condition.Type == "Serving" {
failures = addCatalogConditionFailure(failures, catalog.Name, condition)
}
if condition.Type == "Progressing" && condition.Reason != "Succeeded" {
failures = addCatalogConditionFailure(failures, catalog.Name, condition)
}
}
return failures, nil
}
// isValidImageRef does a simple regex check to validate image refs
func isValidImageRef(ref string) bool {
pattern := `^([a-zA-Z0-9\-\.]+(?::[0-9]+)?/)?([a-z0-9]+(?:[._\-\/][a-z0-9]+)*)(:[\w][\w.-]{0,127})?(?:@sha256:[a-f0-9]{64})?$`
return regexp.MustCompile(pattern).MatchString(ref)
}

View File

@@ -0,0 +1,182 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"context"
"fmt"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/fake"
)
func TestClusterCatalogAnalyzer(t *testing.T) {
gvr := schema.GroupVersionResource{
Group: "olm.operatorframework.io",
Version: "v1",
Resource: "clustercatalogs",
}
scheme := runtime.NewScheme()
dynamicClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
scheme,
map[schema.GroupVersionResource]string{
gvr: "ClusterCatalogList",
},
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "olm.operatorframework.io/v1",
"kind": "ClusterCatalog",
"metadata": map[string]interface{}{
"name": "Valid ClusterCatalog",
},
"spec": map[string]interface{}{
"availabilityMode": "Available",
"source": map[string]interface{}{
"type": "Image",
"image": map[string]interface{}{
"ref": "registry.redhat.io/redhat/community-operator-index:v4.19",
"pollIntervalMinutes": float64(10),
},
},
},
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{
"type": "Progressing",
"status": "True",
"reason": "Succeeded",
},
map[string]interface{}{
"type": "Serving",
"status": "True",
"reason": "Available",
},
},
},
},
},
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "olm.operatorframework.io/v1",
"kind": "ClusterCatalog",
"metadata": map[string]interface{}{
"name": "Invalid availabilityMode",
},
"spec": map[string]interface{}{
"availabilityMode": "test",
"source": map[string]interface{}{
"type": "Image",
"image": map[string]interface{}{
"ref": "registry.redhat.io/redhat/community-operator-index:v4.19",
"pollIntervalMinutes": float64(10),
},
},
},
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{
"type": "Progressing",
"status": "True",
"reason": "Retrying",
},
},
},
},
},
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "olm.operatorframework.io/v1",
"kind": "ClusterCatalog",
"metadata": map[string]interface{}{
"name": "Invalid pollIntervalMinutes",
},
"spec": map[string]interface{}{
"availabilityMode": "Available",
"source": map[string]interface{}{
"type": "Image",
"image": map[string]interface{}{
"ref": "registry.redhat.io/redhat/community-operator-index:v4.19",
"pollIntervalMinutes": float64(0),
},
},
},
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{
"type": "Progressing",
"status": "True",
"reason": "Retrying",
},
},
},
},
},
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "olm.operatorframework.io/v1",
"kind": "ClusterCatalog",
"metadata": map[string]interface{}{
"name": "Invalid image reference",
},
"spec": map[string]interface{}{
"availabilityMode": "Available",
"source": map[string]interface{}{
"type": "Image",
"image": map[string]interface{}{
"ref": "quay.io/test/community-operator-index:v4.19",
"pollIntervalMinutes": float64(10),
},
},
},
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{
"type": "Progressing",
"status": "True",
"reason": "Retrying",
},
},
},
},
},
)
config := common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(),
DynamicClient: dynamicClient,
},
Context: context.Background(),
Namespace: "test",
}
ccAnalyzer := ClusterCatalogAnalyzer{}
results, err := ccAnalyzer.Analyze(config)
for _, res := range results {
fmt.Printf("Result: %s | Failures: %d\n", res.Name, len(res.Error))
for _, err := range res.Error {
fmt.Printf(" - %s\n", err)
}
}
require.NoError(t, err)
require.Equal(t, 3, len(results))
}

View File

@@ -0,0 +1,148 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type ClusterExtensionAnalyzer struct{}
func (ClusterExtensionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "ClusterExtension"
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
"analyzer_name": kind,
})
var clusterExtensionGVR = schema.GroupVersionResource{
Group: "olm.operatorframework.io",
Version: "v1",
Resource: "clusterextensions",
}
if a.Client == nil {
return nil, fmt.Errorf("client is nil in ClusterExtensionAnalyzer")
}
if a.Client.GetDynamicClient() == nil {
return nil, fmt.Errorf("dynamic client is nil in ClusterExtensionAnalyzer")
}
list, err := a.Client.GetDynamicClient().Resource(clusterExtensionGVR).Namespace("").List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
var preAnalysis = map[string]common.PreAnalysis{}
for _, item := range list.Items {
var failures []common.Failure
extension, err := ConvertToClusterExtension(&item)
if err != nil {
continue
}
fmt.Printf("ClusterExtension: %s | Source: %s\n", extension.Name, extension.Spec.Source.Catalog.PackageName)
failures, err = ValidateClusterExtension(failures, extension)
if err != nil {
continue
}
if len(failures) > 0 {
preAnalysis[extension.Name] = common.PreAnalysis{
Extension: *extension,
FailureDetails: failures,
}
AnalyzerErrorsMetric.WithLabelValues(kind, extension.Name, "").Set(float64(len(failures)))
}
}
for key, value := range preAnalysis {
var currentAnalysis = common.Result{
Kind: kind,
Name: key,
Error: value.FailureDetails,
}
parent, found := util.GetParent(a.Client, value.Node.ObjectMeta)
if found {
currentAnalysis.ParentObject = parent
}
a.Results = append(a.Results, currentAnalysis)
}
return a.Results, err
}
func ConvertToClusterExtension(u *unstructured.Unstructured) (*common.ClusterExtension, error) {
var ce common.ClusterExtension
err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &ce)
if err != nil {
return nil, fmt.Errorf("failed to convert to ClusterExtension: %w", err)
}
return &ce, nil
}
func addExtensionConditionFailure(failures []common.Failure, extensionName string, extensionCondition metav1.Condition) []common.Failure {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("OLMv1 ClusterExtension: %s has condition of type %s, reason %s: %s", extensionName, extensionCondition.Type, extensionCondition.Reason, extensionCondition.Message),
Sensitive: []common.Sensitive{
{
Unmasked: extensionName,
Masked: util.MaskString(extensionName),
},
},
})
return failures
}
func addExtensionFailure(failures []common.Failure, extensionName string, err error) []common.Failure {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("%s has error: %s", extensionName, err.Error()),
Sensitive: []common.Sensitive{
{
Unmasked: extensionName,
Masked: util.MaskString(extensionName),
},
},
})
return failures
}
func ValidateClusterExtension(failures []common.Failure, extension *common.ClusterExtension) ([]common.Failure, error) {
if extension.Spec.Source.Catalog != nil && extension.Spec.Source.Catalog.UpgradeConstraintPolicy != "CatalogProvided" && extension.Spec.Source.Catalog.UpgradeConstraintPolicy != "SelfCertified" {
failures = addExtensionFailure(failures, extension.Name, fmt.Errorf("invalid or missing extension.Spec.Source.Catalog.UpgradeConstraintPolicy (expecting 'SelfCertified' or 'CatalogProvided')"))
}
if extension.Spec.Source.SourceType != "Catalog" {
failures = addExtensionFailure(failures, extension.Name, fmt.Errorf("invalid or missing spec.source.sourceType (expecting 'Catalog')"))
}
for _, condition := range extension.Status.Conditions {
if condition.Status != "True" && condition.Type == "Installed" {
failures = addExtensionConditionFailure(failures, extension.Name, condition)
}
if condition.Type == "Progressing" && condition.Reason != "Succeeded" {
failures = addExtensionConditionFailure(failures, extension.Name, condition)
}
}
return failures, nil
}

View File

@@ -0,0 +1,179 @@
/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"context"
"fmt"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/fake"
)
func TestClusterExtensionAnalyzer(t *testing.T) {
gvr := schema.GroupVersionResource{
Group: "olm.operatorframework.io",
Version: "v1",
Resource: "clusterextensions",
}
scheme := runtime.NewScheme()
dynamicClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
scheme,
map[schema.GroupVersionResource]string{
gvr: "ClusterExtensionList",
},
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "olm.operatorframework.io/v1",
"kind": "ClusterExtension",
"metadata": map[string]interface{}{
"name": "Valid SelfCertified ClusterExtension",
},
"spec": map[string]interface{}{
"source": map[string]interface{}{
"sourceType": "Catalog",
"catalog": map[string]interface{}{
"upgradeConstraintPolicy": "SelfCertified",
},
},
},
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{
"type": "Installed",
"status": "True",
"reason": "Succeeded",
},
},
},
},
},
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "olm.operatorframework.io/v1",
"kind": "ClusterExtension",
"metadata": map[string]interface{}{
"name": "Valid CatalogProvided ClusterExtension",
},
"spec": map[string]interface{}{
"source": map[string]interface{}{
"sourceType": "Catalog",
"catalog": map[string]interface{}{
"upgradeConstraintPolicy": "CatalogProvided",
},
},
},
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{
"type": "Installed",
"status": "True",
"reason": "Succeeded",
},
},
},
},
},
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "olm.operatorframework.io/v1",
"kind": "ClusterExtension",
"metadata": map[string]interface{}{
"name": "Invalid UpgradeConstraintPolicy",
},
"spec": map[string]interface{}{
"source": map[string]interface{}{
"sourceType": "Catalog",
"catalog": map[string]interface{}{
"upgradeConstraintPolicy": "InvalidPolicy",
},
},
},
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{
"type": "Progressing",
"status": "True",
"reason": "Retrying",
},
map[string]interface{}{
"type": "Installed",
"status": "False",
"reason": "Failed",
},
},
},
},
},
&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "olm.operatorframework.io/v1",
"kind": "ClusterExtension",
"metadata": map[string]interface{}{
"name": "Invalid SourceType",
},
"spec": map[string]interface{}{
"source": map[string]interface{}{
"sourceType": "Git",
"catalog": map[string]interface{}{
"upgradeConstraintPolicy": "CatalogProvided",
},
},
},
"status": map[string]interface{}{
"conditions": []interface{}{
map[string]interface{}{
"type": "Progressing",
"status": "True",
"reason": "Retrying",
},
map[string]interface{}{
"type": "Installed",
"status": "False",
"reason": "Failed",
},
},
},
},
},
)
config := common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(),
DynamicClient: dynamicClient,
},
Context: context.Background(),
Namespace: "test",
}
ceAnalyzer := ClusterExtensionAnalyzer{}
results, err := ceAnalyzer.Analyze(config)
for _, res := range results {
fmt.Printf("Result: %s | Failures: %d\n", res.Name, len(res.Error))
for _, err := range res.Error {
fmt.Printf(" - %s\n", err)
}
}
require.NoError(t, err)
require.Equal(t, 2, len(results))
}

View File

@@ -0,0 +1,82 @@
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type ClusterServiceVersionAnalyzer struct{}
var csvGVR = schema.GroupVersionResource{
Group: "operators.coreos.com", Version: "v1alpha1", Resource: "clusterserviceversions",
}
func (ClusterServiceVersionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "ClusterServiceVersion"
if a.Client.GetDynamicClient() == nil {
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
}
list, err := a.Client.GetDynamicClient().
Resource(csvGVR).Namespace(metav1.NamespaceAll).
List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
var results []common.Result
for _, item := range list.Items {
ns := item.GetNamespace()
name := item.GetName()
phase, _, _ := unstructured.NestedString(item.Object, "status", "phase")
var failures []common.Failure
if phase != "" && phase != "Succeeded" {
// Superfície de condições para contexto
if conds, _, _ := unstructured.NestedSlice(item.Object, "status", "conditions"); len(conds) > 0 {
if msg := pickWorstCondition(conds); msg != "" {
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q: %s", phase, msg)})
}
} else {
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q (see status.conditions)", phase)})
}
}
if len(failures) > 0 {
results = append(results, common.Result{
Kind: kind,
Name: ns + "/" + name,
Error: failures,
})
}
}
return results, nil
}
// reaproveitamos o heurístico já usado em outros pontos
func pickWorstCondition(conds []interface{}) string {
for _, c := range conds {
m, ok := c.(map[string]any)
if !ok {
continue
}
if s, _ := m["status"].(string); s == "True" {
continue
}
r, _ := m["reason"].(string)
msg, _ := m["message"].(string)
if r == "" && msg == "" {
continue
}
if r != "" && msg != "" {
return r + ": " + msg
}
return r + msg
}
return ""
}

View File

@@ -0,0 +1,78 @@
package analyzer
import (
"context"
"strings"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
)
func TestClusterServiceVersionAnalyzer(t *testing.T) {
ok := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1alpha1",
"kind": "ClusterServiceVersion",
"metadata": map[string]any{
"name": "ok",
"namespace": "ns1",
},
"status": map[string]any{"phase": "Succeeded"},
},
}
bad := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1alpha1",
"kind": "ClusterServiceVersion",
"metadata": map[string]any{
"name": "bad",
"namespace": "ns1",
},
"status": map[string]any{
"phase": "Failed",
// IMPORTANT: conditions must be []interface{}, not []map[string]any
"conditions": []interface{}{
map[string]any{
"status": "False",
"reason": "ErrorResolving",
"message": "missing dep",
},
},
},
},
}
listKinds := map[schema.GroupVersionResource]string{
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "clusterserviceversions"}: "ClusterServiceVersionList",
}
// Use a non-nil scheme with dynamicfake
scheme := runtime.NewScheme()
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ok, bad)
a := common.Analyzer{
Context: context.TODO(),
Client: &kubernetes.Client{DynamicClient: dc},
}
res, err := (ClusterServiceVersionAnalyzer{}).Analyze(a)
if err != nil {
t.Fatalf("Analyze error: %v", err)
}
if len(res) != 1 {
t.Fatalf("expected 1 result, got %d", len(res))
}
if res[0].Kind != "ClusterServiceVersion" || !strings.Contains(res[0].Name, "ns1/bad") {
t.Fatalf("unexpected result: %#v", res[0])
}
if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "missing dep") {
t.Fatalf("expected 'missing dep' in failure, got %#v", res[0].Error)
}
}

125
pkg/analyzer/configmap.go Normal file
View File

@@ -0,0 +1,125 @@
/*
Copyright 2024 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type ConfigMapAnalyzer struct{}
func (ConfigMapAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "ConfigMap"
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
"analyzer_name": kind,
})
// Get all ConfigMaps in the namespace
configMaps, err := a.Client.GetClient().CoreV1().ConfigMaps(a.Namespace).List(a.Context, metav1.ListOptions{
LabelSelector: a.LabelSelector,
})
if err != nil {
return nil, err
}
// Get all Pods to check ConfigMap usage
pods, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
var results []common.Result
// Track which ConfigMaps are used
usedConfigMaps := make(map[string]bool)
configMapUsage := make(map[string][]string) // maps ConfigMap name to list of pods using it
// Analyze ConfigMap usage in Pods
for _, pod := range pods.Items {
// Check volume mounts
for _, volume := range pod.Spec.Volumes {
if volume.ConfigMap != nil {
usedConfigMaps[volume.ConfigMap.Name] = true
configMapUsage[volume.ConfigMap.Name] = append(configMapUsage[volume.ConfigMap.Name], pod.Name)
}
}
// Check environment variables
for _, container := range pod.Spec.Containers {
for _, env := range container.EnvFrom {
if env.ConfigMapRef != nil {
usedConfigMaps[env.ConfigMapRef.Name] = true
configMapUsage[env.ConfigMapRef.Name] = append(configMapUsage[env.ConfigMapRef.Name], pod.Name)
}
}
for _, env := range container.Env {
if env.ValueFrom != nil && env.ValueFrom.ConfigMapKeyRef != nil {
usedConfigMaps[env.ValueFrom.ConfigMapKeyRef.Name] = true
configMapUsage[env.ValueFrom.ConfigMapKeyRef.Name] = append(configMapUsage[env.ValueFrom.ConfigMapKeyRef.Name], pod.Name)
}
}
}
}
// Analyze each ConfigMap
for _, cm := range configMaps.Items {
var failures []common.Failure
// Check for unused ConfigMaps
if !usedConfigMaps[cm.Name] {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("ConfigMap %s is not used by any pods in the namespace", cm.Name),
Sensitive: []common.Sensitive{},
})
}
// Check for empty ConfigMaps
if len(cm.Data) == 0 && len(cm.BinaryData) == 0 {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("ConfigMap %s is empty", cm.Name),
Sensitive: []common.Sensitive{},
})
}
// Check for large ConfigMaps (over 1MB)
totalSize := 0
for _, value := range cm.Data {
totalSize += len(value)
}
for _, value := range cm.BinaryData {
totalSize += len(value)
}
if totalSize > 1024*1024 { // 1MB
failures = append(failures, common.Failure{
Text: fmt.Sprintf("ConfigMap %s is larger than 1MB (%d bytes)", cm.Name, totalSize),
Sensitive: []common.Sensitive{},
})
}
if len(failures) > 0 {
results = append(results, common.Result{
Kind: kind,
Name: fmt.Sprintf("%s/%s", cm.Namespace, cm.Name),
Error: failures,
})
AnalyzerErrorsMetric.WithLabelValues(kind, cm.Name, cm.Namespace).Set(float64(len(failures)))
}
}
return results, nil
}

View File

@@ -0,0 +1,149 @@
/*
Copyright 2024 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"context"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestConfigMapAnalyzer(t *testing.T) {
tests := []struct {
name string
namespace string
configMaps []v1.ConfigMap
pods []v1.Pod
expectedErrors int
}{
{
name: "unused configmap",
namespace: "default",
configMaps: []v1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "unused-cm",
Namespace: "default",
},
Data: map[string]string{
"key": "value",
},
},
},
expectedErrors: 1,
},
{
name: "empty configmap",
namespace: "default",
configMaps: []v1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "empty-cm",
Namespace: "default",
},
},
},
expectedErrors: 1,
},
{
name: "large configmap",
namespace: "default",
configMaps: []v1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "large-cm",
Namespace: "default",
},
Data: map[string]string{
"key": string(make([]byte, 1024*1024+1)), // 1MB + 1 byte
},
},
},
expectedErrors: 1,
},
{
name: "used configmap",
namespace: "default",
configMaps: []v1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "used-cm",
Namespace: "default",
},
Data: map[string]string{
"key": "value",
},
},
},
pods: []v1.Pod{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "default",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "test-container",
EnvFrom: []v1.EnvFromSource{
{
ConfigMapRef: &v1.ConfigMapEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "used-cm",
},
},
},
},
},
},
},
},
},
expectedErrors: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := fake.NewSimpleClientset()
// Create test resources
for _, cm := range tt.configMaps {
_, err := client.CoreV1().ConfigMaps(tt.namespace).Create(context.TODO(), &cm, metav1.CreateOptions{})
assert.NoError(t, err)
}
for _, pod := range tt.pods {
_, err := client.CoreV1().Pods(tt.namespace).Create(context.TODO(), &pod, metav1.CreateOptions{})
assert.NoError(t, err)
}
analyzer := ConfigMapAnalyzer{}
results, err := analyzer.Analyze(common.Analyzer{
Client: &kubernetes.Client{Client: client},
Context: context.TODO(),
Namespace: tt.namespace,
})
assert.NoError(t, err)
assert.Equal(t, tt.expectedErrors, len(results))
})
}
}

View File

@@ -22,179 +22,274 @@ import (
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/stretchr/testify/require"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestCronJobAnalyzer(t *testing.T) {
suspend := new(bool)
*suspend = true
invalidStartingDeadline := new(int64)
*invalidStartingDeadline = -7
validStartingDeadline := new(int64)
*validStartingDeadline = 7
config := common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "CJ1",
// This CronJob won't be list because of namespace filtering.
Namespace: "test",
},
},
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "CJ2",
Namespace: "default",
},
// A suspended CronJob will contribute to failures.
Spec: batchv1.CronJobSpec{
Suspend: suspend,
},
},
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "CJ3",
Namespace: "default",
},
Spec: batchv1.CronJobSpec{
// Valid schedule
Schedule: "*/1 * * * *",
// Negative starting deadline
StartingDeadlineSeconds: invalidStartingDeadline,
},
},
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "CJ4",
Namespace: "default",
},
Spec: batchv1.CronJobSpec{
// Invalid schedule
Schedule: "*** * * * *",
},
},
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "CJ5",
Namespace: "default",
},
Spec: batchv1.CronJobSpec{
// Valid schedule
Schedule: "*/1 * * * *",
// Positive starting deadline shouldn't be any problem.
StartingDeadlineSeconds: validStartingDeadline,
},
},
&batchv1.CronJob{
// This cronjob shouldn't contribute to any failures.
ObjectMeta: metav1.ObjectMeta{
Name: "successful-cronjob",
Namespace: "default",
Annotations: map[string]string{
"analysisDate": "2022-04-01",
},
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.CronJobSpec{
Schedule: "*/1 * * * *",
ConcurrencyPolicy: "Allow",
JobTemplate: batchv1.JobTemplateSpec{
tests := []struct {
name string
config common.Analyzer
expectations []struct {
name string
failuresCount int
}
}{
{
name: "Suspended CronJob",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "example-app",
},
Name: "suspended-job",
Namespace: "default",
},
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "example-container",
Image: "nginx",
},
},
RestartPolicy: v1.RestartPolicyOnFailure,
},
},
Spec: batchv1.CronJobSpec{
Schedule: "*/5 * * * *",
Suspend: boolPtr(true),
},
},
},
),
},
),
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/suspended-job",
failuresCount: 1, // One failure for being suspended
},
},
},
{
name: "Invalid schedule format",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "invalid-schedule",
Namespace: "default",
},
Spec: batchv1.CronJobSpec{
Schedule: "invalid-cron", // Invalid cron format
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/invalid-schedule",
failuresCount: 1, // One failure for invalid schedule
},
},
},
{
name: "Negative starting deadline",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "negative-deadline",
Namespace: "default",
},
Spec: batchv1.CronJobSpec{
Schedule: "*/5 * * * *",
StartingDeadlineSeconds: int64Ptr(-60), // Negative deadline
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/negative-deadline",
failuresCount: 1, // One failure for negative deadline
},
},
},
{
name: "Valid CronJob",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-job",
Namespace: "default",
},
Spec: batchv1.CronJobSpec{
Schedule: "*/5 * * * *", // Valid cron format
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
// No expectations for valid job
},
},
{
name: "Multiple issues",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "multiple-issues",
Namespace: "default",
},
Spec: batchv1.CronJobSpec{
Schedule: "invalid-cron",
StartingDeadlineSeconds: int64Ptr(-60),
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/multiple-issues",
failuresCount: 2, // Two failures: invalid schedule and negative deadline
},
},
},
Context: context.Background(),
Namespace: "default",
}
cjAnalyzer := CronJobAnalyzer{}
results, err := cjAnalyzer.Analyze(config)
require.NoError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
analyzer := CronJobAnalyzer{}
results, err := analyzer.Analyze(tt.config)
require.NoError(t, err)
require.Len(t, results, len(tt.expectations))
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
// Sort results by name for consistent comparison
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
expectations := []string{
"default/CJ2",
"default/CJ3",
"default/CJ4",
}
require.Equal(t, len(expectations), len(results))
for i, result := range results {
require.Equal(t, expectations[i], result.Name)
for i, expectation := range tt.expectations {
require.Equal(t, expectation.name, results[i].Name)
require.Len(t, results[i].Error, expectation.failuresCount)
}
})
}
}
func TestCronJobAnalyzerLabelSelectorFiltering(t *testing.T) {
suspend := new(bool)
*suspend = true
invalidStartingDeadline := new(int64)
*invalidStartingDeadline = -7
validStartingDeadline := new(int64)
*validStartingDeadline = 7
func TestCronJobAnalyzerLabelSelector(t *testing.T) {
clientSet := fake.NewSimpleClientset(
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "job-with-label",
Namespace: "default",
Labels: map[string]string{
"app": "test",
},
},
Spec: batchv1.CronJobSpec{
Schedule: "invalid-cron", // This should trigger a failure
},
},
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "job-without-label",
Namespace: "default",
},
Spec: batchv1.CronJobSpec{
Schedule: "invalid-cron", // This should trigger a failure
},
},
)
// Test with label selector
config := common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "CJ1",
Namespace: "default",
Labels: map[string]string{
"app": "cronjob",
},
},
},
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "CJ2",
Namespace: "default",
},
},
),
Client: clientSet,
},
Context: context.Background(),
Namespace: "default",
LabelSelector: "app=cronjob",
LabelSelector: "app=test",
}
cjAnalyzer := CronJobAnalyzer{}
results, err := cjAnalyzer.Analyze(config)
analyzer := CronJobAnalyzer{}
results, err := analyzer.Analyze(config)
require.NoError(t, err)
require.Equal(t, 1, len(results))
require.Equal(t, "default/CJ1", results[0].Name)
require.Equal(t, "default/job-with-label", results[0].Name)
}
func TestCheckCronScheduleIsValid(t *testing.T) {
tests := []struct {
name string
schedule string
wantErr bool
}{
{
name: "Valid schedule - every 5 minutes",
schedule: "*/5 * * * *",
wantErr: false,
},
{
name: "Valid schedule - specific time",
schedule: "0 2 * * *",
wantErr: false,
},
{
name: "Valid schedule - complex",
schedule: "0 0 1,15 * 3",
wantErr: false,
},
{
name: "Invalid schedule - wrong format",
schedule: "invalid-cron",
wantErr: true,
},
{
name: "Invalid schedule - too many fields",
schedule: "* * * * * *",
wantErr: true,
},
{
name: "Invalid schedule - empty string",
schedule: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := CheckCronScheduleIsValid(tt.schedule)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -54,22 +54,41 @@ func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
for _, deployment := range deployments.Items {
var failures []common.Failure
if *deployment.Spec.Replicas != deployment.Status.Replicas {
doc := apiDoc.GetApiDocV2("spec.replicas")
if *deployment.Spec.Replicas != deployment.Status.ReadyReplicas {
if deployment.Status.Replicas > *deployment.Spec.Replicas {
doc := apiDoc.GetApiDocV2("spec.replicas")
failures = append(failures, common.Failure{
Text: fmt.Sprintf("Deployment %s/%s has %d replicas but %d are available", deployment.Namespace, deployment.Name, *deployment.Spec.Replicas, deployment.Status.Replicas),
KubernetesDoc: doc,
Sensitive: []common.Sensitive{
{
Unmasked: deployment.Namespace,
Masked: util.MaskString(deployment.Namespace),
},
{
Unmasked: deployment.Name,
Masked: util.MaskString(deployment.Name),
},
}})
failures = append(failures, common.Failure{
Text: fmt.Sprintf("Deployment %s/%s has %d replicas in spec but %d replicas in status because status field is not updated yet after scaling and %d replicas are available with status running", deployment.Namespace, deployment.Name, *deployment.Spec.Replicas, deployment.Status.Replicas, deployment.Status.ReadyReplicas),
KubernetesDoc: doc,
Sensitive: []common.Sensitive{
{
Unmasked: deployment.Namespace,
Masked: util.MaskString(deployment.Namespace),
},
{
Unmasked: deployment.Name,
Masked: util.MaskString(deployment.Name),
},
}})
} else {
doc := apiDoc.GetApiDocV2("spec.replicas")
failures = append(failures, common.Failure{
Text: fmt.Sprintf("Deployment %s/%s has %d replicas but %d are available with status running", deployment.Namespace, deployment.Name, *deployment.Spec.Replicas, deployment.Status.ReadyReplicas),
KubernetesDoc: doc,
Sensitive: []common.Sensitive{
{
Unmasked: deployment.Namespace,
Masked: util.MaskString(deployment.Namespace),
},
{
Unmasked: deployment.Name,
Masked: util.MaskString(deployment.Name),
},
}})
}
}
if len(failures) > 0 {
preAnalysis[fmt.Sprintf("%s/%s", deployment.Namespace, deployment.Name)] = common.PreAnalysis{

137
pkg/analyzer/events_test.go Normal file
View File

@@ -0,0 +1,137 @@
package analyzer_test
import (
"context"
"errors"
"testing"
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
)
func FetchLatestEvent(ctx context.Context, client kubernetes.Interface, namespace, eventName string) (*v1.Event, error) {
// List events in the specified namespace
events, err := client.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
var latestEvent *v1.Event
for _, event := range events.Items {
// Check if the event name matches the requested name (partial match)
if eventName == "" || event.Name == eventName {
if latestEvent == nil || event.LastTimestamp.Time.After(latestEvent.LastTimestamp.Time) {
latestEvent = &event
}
}
}
// If no matching event is found, return an error
if latestEvent == nil {
return nil, errors.New("no matching events found")
}
return latestEvent, nil
}
func TestFetchLatestEvent(t *testing.T) {
fakeClient := fake.NewSimpleClientset()
// Simulating events with different timestamps
event1 := &v1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: "test-event-1",
Namespace: "default",
},
LastTimestamp: metav1.Time{Time: time.Now()},
}
event2 := &v1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: "test-event-2",
Namespace: "default",
},
LastTimestamp: metav1.Time{Time: time.Now().Add(-time.Hour)}, // event1 should be fetched as it's newer
}
// ✅ Explicitly ensure namespace exists
_, err := fakeClient.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: "default"},
}, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create namespace: %v", err)
}
// ✅ Ensure events are properly created and stored in the fake client
_, err = fakeClient.CoreV1().Events("default").Create(context.TODO(), event1, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create event1: %v", err)
}
_, err = fakeClient.CoreV1().Events("default").Create(context.TODO(), event2, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create event2: %v", err)
}
// 🔍 Debug: Check if events exist before running FetchLatestEvent
storedEvents, _ := fakeClient.CoreV1().Events("default").List(context.TODO(), metav1.ListOptions{})
if len(storedEvents.Items) == 0 {
t.Fatal("No events were found in the fake client. Ensure event creation is working correctly.")
}
// Test cases
tests := []struct {
name string
namespace string
nameToFind string
expected *v1.Event
shouldFail bool
}{
{
name: "Valid case - fetch the latest event",
namespace: "default",
nameToFind: "test-event-1", // Match exact event name
expected: event1, // event1 has the latest timestamp
shouldFail: false,
},
{
name: "Nonexistent event",
namespace: "default",
nameToFind: "nonexistent-event", // Should not exist
expected: nil,
shouldFail: true,
},
{
name: "Nonexistent namespace",
namespace: "nonexistent-namespace", // Namespace doesn't exist
nameToFind: "test-event",
expected: nil,
shouldFail: true,
},
}
// Run tests
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Call the function to fetch the latest event
event, err := FetchLatestEvent(context.TODO(), fakeClient, tt.namespace, tt.nameToFind)
// Handle the expected outcomes based on the test case
if tt.shouldFail {
if err == nil {
t.Error("Expected an error, but got nil")
}
if event != nil {
t.Errorf("Expected nil event, but got event: %s", event.Name)
}
} else {
if err != nil {
t.Errorf("Expected no error, but got %v", err)
}
if event != nil && event.Name != tt.expected.Name {
t.Errorf("Expected event name %s, got %s", tt.expected.Name, event.Name)
}
}
})
}
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -34,7 +35,7 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
Kind: kind,
ApiVersion: schema.GroupVersion{
Group: "autoscaling",
Version: "v1",
Version: "v2",
},
OpenapiSchema: a.OpenapiSchema,
}
@@ -56,11 +57,22 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
//check the error from status field
conditions := hpa.Status.Conditions
for _, condition := range conditions {
if condition.Status != "True" {
failures = append(failures, common.Failure{
Text: condition.Message,
Sensitive: []common.Sensitive{},
})
// https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#appendix-horizontal-pod-autoscaler-status-conditions
switch condition.Type {
case autoscalingv2.ScalingLimited:
if condition.Status == corev1.ConditionTrue {
failures = append(failures, common.Failure{
Text: condition.Message,
Sensitive: []common.Sensitive{},
})
}
default:
if condition.Status == corev1.ConditionFalse {
failures = append(failures, common.Failure{
Text: condition.Message,
Sensitive: []common.Sensitive{},
})
}
}
}

View File

@@ -735,3 +735,87 @@ 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,226 +15,243 @@ package analyzer
import (
"context"
"sort"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestIngressAnalyzer(t *testing.T) {
validIgClassName := new(string)
*validIgClassName = "valid-ingress-class"
var igRule networkingv1.IngressRule
httpRule := networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
// This service exists.
Name: "Service1",
},
},
},
{
Path: "/test1",
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
// This service is in the test namespace
// Hence, it won't be discovered.
Name: "Service2",
},
},
},
{
Path: "/test2",
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
// This service doesn't exist.
Name: "Service3",
},
},
},
},
}
igRule.IngressRuleValue.HTTP = &httpRule
config := common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&networkingv1.Ingress{
// Doesn't specify an ingress class.
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress1",
Namespace: "default",
},
},
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress2",
Namespace: "default",
// Specify an invalid ingress class name using annotations.
Annotations: map[string]string{
"kubernetes.io/ingress.class": "invalid-class",
},
},
},
&networkingv1.Ingress{
// Namespace filtering.
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress3",
Namespace: "test",
},
},
&networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: *validIgClassName,
},
},
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress4",
Namespace: "default",
// Specify valid ingress class name using annotations.
Annotations: map[string]string{
"kubernetes.io/ingress.class": *validIgClassName,
},
},
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "Service1",
Namespace: "default",
},
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
// Namespace filtering.
Name: "Service2",
Namespace: "test",
},
},
&v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "Secret1",
Namespace: "default",
},
},
&v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "Secret2",
Namespace: "test",
},
},
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress5",
Namespace: "default",
},
// Specify valid ingress class name in spec.
Spec: networkingv1.IngressSpec{
IngressClassName: validIgClassName,
Rules: []networkingv1.IngressRule{
igRule,
},
TLS: []networkingv1.IngressTLS{
{
// This won't contribute to any failures.
SecretName: "Secret1",
},
{
// This secret won't be discovered because of namespace filtering.
SecretName: "Secret2",
},
{
// This secret doesn't exist.
SecretName: "Secret3",
},
},
},
},
),
},
Context: context.Background(),
Namespace: "default",
}
igAnalyzer := IngressAnalyzer{}
results, err := igAnalyzer.Analyze(config)
require.NoError(t, err)
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
expectations := []struct {
name string
failuresCount int
// Create test cases
testCases := []struct {
name string
ingress *networkingv1.Ingress
expectedIssues []string
}{
{
name: "default/Ingress1",
failuresCount: 1,
name: "Non-existent backend service",
ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "default",
},
Spec: networkingv1.IngressSpec{
Rules: []networkingv1.IngressRule{
{
Host: "example.com",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "non-existent-service",
Port: networkingv1.ServiceBackendPort{
Number: 80,
},
},
},
},
},
},
},
},
},
},
},
expectedIssues: []string{
"Ingress default/test-ingress does not specify an Ingress class.",
"Ingress uses the service default/non-existent-service which does not exist.",
},
},
{
name: "default/Ingress2",
failuresCount: 1,
name: "Non-existent TLS secret",
ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress-tls",
Namespace: "default",
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{
{
Hosts: []string{"example.com"},
SecretName: "non-existent-secret",
},
},
Rules: []networkingv1.IngressRule{
{
Host: "example.com",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test-service",
Port: networkingv1.ServiceBackendPort{
Number: 80,
},
},
},
},
},
},
},
},
},
},
},
expectedIssues: []string{
"Ingress default/test-ingress-tls does not specify an Ingress class.",
"Ingress uses the service default/test-service which does not exist.",
"Ingress uses the secret default/non-existent-secret as a TLS certificate which does not exist.",
},
},
{
name: "default/Ingress5",
failuresCount: 4,
name: "Multiple issues",
ingress: &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress-multi",
Namespace: "default",
},
Spec: networkingv1.IngressSpec{
TLS: []networkingv1.IngressTLS{
{
Hosts: []string{"example.com"},
SecretName: "non-existent-secret",
},
},
Rules: []networkingv1.IngressRule{
{
Host: "example.com",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{
Path: "/",
Backend: networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "non-existent-service",
Port: networkingv1.ServiceBackendPort{
Number: 80,
},
},
},
},
},
},
},
},
},
},
},
expectedIssues: []string{
"Ingress default/test-ingress-multi does not specify an Ingress class.",
"Ingress uses the service default/non-existent-service which does not exist.",
"Ingress uses the secret default/non-existent-secret as a TLS certificate which does not exist.",
},
},
}
require.Equal(t, len(expectations), len(results))
// Run test cases
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a new context and clientset for each test case
ctx := context.Background()
clientset := fake.NewSimpleClientset()
for i, result := range results {
require.Equal(t, expectations[i].name, result.Name)
require.Equal(t, expectations[i].failuresCount, len(result.Error))
// Create the ingress in the fake clientset
_, err := clientset.NetworkingV1().Ingresses(tc.ingress.Namespace).Create(ctx, tc.ingress, metav1.CreateOptions{})
assert.NoError(t, err)
// Create the analyzer configuration
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: ctx,
Namespace: tc.ingress.Namespace,
}
// Create the analyzer and run analysis
analyzer := IngressAnalyzer{}
results, err := analyzer.Analyze(config)
assert.NoError(t, err)
// Check that we got the expected number of issues
assert.Len(t, results, 1, "Expected 1 result")
result := results[0]
assert.Len(t, result.Error, len(tc.expectedIssues), "Expected %d issues, got %d", len(tc.expectedIssues), len(result.Error))
// Check that each expected issue is present
for _, expectedIssue := range tc.expectedIssues {
found := false
for _, failure := range result.Error {
if failure.Text == expectedIssue {
found = true
break
}
}
assert.True(t, found, "Expected to find issue: %s", expectedIssue)
}
})
}
}
func TestIngressAnalyzerLabelSelectorFiltering(t *testing.T) {
validIgClassName := new(string)
*validIgClassName = "valid-ingress-class"
func TestIngressAnalyzerLabelSelector(t *testing.T) {
clientSet := fake.NewSimpleClientset(
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-with-label",
Namespace: "default",
Labels: map[string]string{
"app": "test",
},
},
Spec: networkingv1.IngressSpec{
// Missing ingress class to trigger a failure
},
},
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ingress-without-label",
Namespace: "default",
},
Spec: networkingv1.IngressSpec{
// Missing ingress class to trigger a failure
},
},
)
// Test with label selector
config := common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress1",
Namespace: "default",
Labels: map[string]string{
"app": "ingress",
},
},
},
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "Ingress2",
Namespace: "default",
},
},
),
Client: clientSet,
},
Context: context.Background(),
Namespace: "default",
LabelSelector: "app=ingress",
LabelSelector: "app=test",
}
igAnalyzer := IngressAnalyzer{}
results, err := igAnalyzer.Analyze(config)
analyzer := IngressAnalyzer{}
results, err := analyzer.Analyze(config)
require.NoError(t, err)
require.Equal(t, 1, len(results))
require.Equal(t, "default/Ingress1", results[0].Name)
require.Equal(t, "default/ingress-with-label", results[0].Name)
}
// Helper functions
func strPtr(s string) *string {
return &s
}
func pathTypePtr(p networkingv1.PathType) *networkingv1.PathType {
return &p
}

View File

@@ -0,0 +1,75 @@
package analyzer
import (
"context"
"strings"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
)
func TestInstallPlanAnalyzer(t *testing.T) {
ok := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1alpha1",
"kind": "InstallPlan",
"metadata": map[string]any{
"name": "ip-ok",
"namespace": "ns1",
},
"status": map[string]any{"phase": "Complete"},
},
}
bad := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1alpha1",
"kind": "InstallPlan",
"metadata": map[string]any{
"name": "ip-bad",
"namespace": "ns1",
},
"status": map[string]any{
"phase": "Failed",
"conditions": []interface{}{
map[string]any{
"reason": "ExecutionError",
"message": "something went wrong",
},
},
},
},
}
listKinds := map[schema.GroupVersionResource]string{
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "installplans"}: "InstallPlanList",
}
scheme := runtime.NewScheme()
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ok, bad)
a := common.Analyzer{
Context: context.TODO(),
Client: &kubernetes.Client{DynamicClient: dc},
}
res, err := (InstallPlanAnalyzer{}).Analyze(a)
if err != nil {
t.Fatalf("Analyze error: %v", err)
}
if len(res) != 1 {
t.Fatalf("expected 1 result, got %d", len(res))
}
if res[0].Kind != "InstallPlan" || !strings.Contains(res[0].Name, "ns1/ip-bad") {
t.Fatalf("unexpected result: %#v", res[0])
}
if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "ExecutionError") {
t.Fatalf("expected 'ExecutionError' in failure, got %#v", res[0].Error)
}
}

View File

@@ -0,0 +1,72 @@
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type InstallPlanAnalyzer struct{}
var ipGVR = schema.GroupVersionResource{
Group: "operators.coreos.com", Version: "v1alpha1", Resource: "installplans",
}
func (InstallPlanAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "InstallPlan"
if a.Client.GetDynamicClient() == nil {
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
}
list, err := a.Client.GetDynamicClient().
Resource(ipGVR).Namespace(metav1.NamespaceAll).
List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
var results []common.Result
for _, item := range list.Items {
ns, name := item.GetNamespace(), item.GetName()
phase, _, _ := unstructured.NestedString(item.Object, "status", "phase")
var failures []common.Failure
if phase != "" && phase != "Complete" {
reason := firstCondStr(&item, "reason")
msg := firstCondStr(&item, "message")
switch {
case reason != "" && msg != "":
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q: %s: %s", phase, reason, msg)})
case reason != "" || msg != "":
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q: %s%s", phase, reason, msg)})
default:
failures = append(failures, common.Failure{Text: fmt.Sprintf("phase=%q (approval/manual? check status.conditions)", phase)})
}
}
if len(failures) > 0 {
results = append(results, common.Result{
Kind: kind,
Name: ns + "/" + name,
Error: failures,
})
}
}
return results, nil
}
func firstCondStr(u *unstructured.Unstructured, field string) string {
conds, _, _ := unstructured.NestedSlice(u.Object, "status", "conditions")
if len(conds) == 0 {
return ""
}
m, _ := conds[0].(map[string]any)
if m == nil {
return ""
}
v, _ := m[field].(string)
return v
}

107
pkg/analyzer/job.go Normal file
View File

@@ -0,0 +1,107 @@
/*
Copyright 2025 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type JobAnalyzer struct{}
func (analyzer JobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "Job"
apiDoc := kubernetes.K8sApiReference{
Kind: kind,
ApiVersion: schema.GroupVersion{
Group: "batch",
Version: "v1",
},
OpenapiSchema: a.OpenapiSchema,
}
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
"analyzer_name": kind,
})
JobList, err := a.Client.GetClient().BatchV1().Jobs(a.Namespace).List(a.Context, v1.ListOptions{LabelSelector: a.LabelSelector})
if err != nil {
return nil, err
}
var preAnalysis = map[string]common.PreAnalysis{}
for _, Job := range JobList.Items {
var failures []common.Failure
if Job.Spec.Suspend != nil && *Job.Spec.Suspend {
doc := apiDoc.GetApiDocV2("spec.suspend")
failures = append(failures, common.Failure{
Text: fmt.Sprintf("Job %s is suspended", Job.Name),
KubernetesDoc: doc,
Sensitive: []common.Sensitive{
{
Unmasked: Job.Namespace,
Masked: util.MaskString(Job.Namespace),
},
{
Unmasked: Job.Name,
Masked: util.MaskString(Job.Name),
},
},
})
}
if Job.Status.Failed > 0 {
doc := apiDoc.GetApiDocV2("status.failed")
failures = append(failures, common.Failure{
Text: fmt.Sprintf("Job %s has failed", Job.Name),
KubernetesDoc: doc,
Sensitive: []common.Sensitive{
{
Unmasked: Job.Namespace,
Masked: util.MaskString(Job.Namespace),
},
{
Unmasked: Job.Name,
Masked: util.MaskString(Job.Name),
},
},
})
}
if len(failures) > 0 {
preAnalysis[fmt.Sprintf("%s/%s", Job.Namespace, Job.Name)] = common.PreAnalysis{
FailureDetails: failures,
}
AnalyzerErrorsMetric.WithLabelValues(kind, Job.Name, Job.Namespace).Set(float64(len(failures)))
}
}
for key, value := range preAnalysis {
currentAnalysis := common.Result{
Kind: kind,
Name: key,
Error: value.FailureDetails,
}
a.Results = append(a.Results, currentAnalysis)
}
return a.Results, nil
}

215
pkg/analyzer/job_test.go Normal file
View File

@@ -0,0 +1,215 @@
/*
Copyright 2025 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"context"
"sort"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/stretchr/testify/require"
batchv1 "k8s.io/api/batch/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestJobAnalyzer(t *testing.T) {
tests := []struct {
name string
config common.Analyzer
expectations []struct {
name string
failuresCount int
}
}{
{
name: "Suspended Job",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "suspended-job",
Namespace: "default",
},
Spec: batchv1.JobSpec{
Suspend: boolPtr(true),
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/suspended-job",
failuresCount: 1, // One failure for being suspended
},
},
},
{
name: "Failed Job",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "failed-job",
Namespace: "default",
},
Spec: batchv1.JobSpec{},
Status: batchv1.JobStatus{
Failed: 1,
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/failed-job",
failuresCount: 1, // One failure for failed job
},
},
},
{
name: "Valid Job",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "valid-job",
Namespace: "default",
},
Spec: batchv1.JobSpec{},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
// No expectations for valid job
},
},
{
name: "Multiple issues",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "multiple-issues",
Namespace: "default",
},
Spec: batchv1.JobSpec{
Suspend: boolPtr(true),
},
Status: batchv1.JobStatus{
Failed: 1,
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/multiple-issues",
failuresCount: 2, // Two failures: suspended and failed job
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
analyzer := JobAnalyzer{}
results, err := analyzer.Analyze(tt.config)
require.NoError(t, err)
require.Len(t, results, len(tt.expectations))
// Sort results by name for consistent comparison
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
for i, expectation := range tt.expectations {
require.Equal(t, expectation.name, results[i].Name)
require.Len(t, results[i].Error, expectation.failuresCount)
}
})
}
}
func TestJobAnalyzerLabelSelector(t *testing.T) {
clientSet := fake.NewSimpleClientset(
&batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "job-with-label",
Namespace: "default",
Labels: map[string]string{
"app": "test",
},
},
Spec: batchv1.JobSpec{},
Status: batchv1.JobStatus{
Failed: 1,
},
},
&batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "job-without-label",
Namespace: "default",
},
Spec: batchv1.JobSpec{},
},
)
// Test with label selector
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientSet,
},
Context: context.Background(),
Namespace: "default",
LabelSelector: "app=test",
}
analyzer := JobAnalyzer{}
results, err := analyzer.Analyze(config)
require.NoError(t, err)
require.Equal(t, 1, len(results))
require.Equal(t, "default/job-with-label", results[0].Name)
}

View File

@@ -46,15 +46,17 @@ func (NodeAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
// https://kubernetes.io/docs/concepts/architecture/nodes/#condition
switch nodeCondition.Type {
case v1.NodeReady:
if nodeCondition.Status == v1.ConditionTrue {
break
if nodeCondition.Status != v1.ConditionTrue {
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
}
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
// k3s `EtcdIsVoter`` should not be reported as an error
case v1.NodeConditionType("EtcdIsVoter"):
break
default:
if nodeCondition.Status != v1.ConditionFalse {
// For other conditions:
// - Report True or Unknown status as failures (for standard conditions)
// - Report any unknown condition type as a failure
if nodeCondition.Status == v1.ConditionTrue || nodeCondition.Status == v1.ConditionUnknown || !isKnownNodeConditionType(nodeCondition.Type) {
failures = addNodeConditionFailure(failures, node.Name, nodeCondition)
}
}
@@ -99,3 +101,17 @@ func addNodeConditionFailure(failures []common.Failure, nodeName string, nodeCon
})
return failures
}
// isKnownNodeConditionType checks if the condition type is a standard Kubernetes node condition
func isKnownNodeConditionType(conditionType v1.NodeConditionType) bool {
switch conditionType {
case v1.NodeReady,
v1.NodeMemoryPressure,
v1.NodeDiskPressure,
v1.NodePIDPressure,
v1.NodeNetworkUnavailable:
return true
default:
return false
}
}

View File

@@ -0,0 +1,46 @@
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type OperatorGroupAnalyzer struct{}
var ogGVR = schema.GroupVersionResource{
Group: "operators.coreos.com", Version: "v1", Resource: "operatorgroups",
}
func (OperatorGroupAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "OperatorGroup"
if a.Client.GetDynamicClient() == nil {
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
}
list, err := a.Client.GetDynamicClient().
Resource(ogGVR).Namespace(metav1.NamespaceAll).
List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
countByNS := map[string]int{}
for _, it := range list.Items {
countByNS[it.GetNamespace()]++
}
var results []common.Result
for ns, n := range countByNS {
if n > 1 {
results = append(results, common.Result{
Kind: kind,
Name: ns,
Error: []common.Failure{{Text: fmt.Sprintf("%d OperatorGroups in namespace; this can break CSV resolution", n)}},
})
}
}
return results, nil
}

View File

@@ -0,0 +1,70 @@
package analyzer
import (
"context"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
)
func TestOperatorGroupAnalyzer(t *testing.T) {
og1 := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1",
"kind": "OperatorGroup",
"metadata": map[string]any{
"name": "og-1",
"namespace": "ns-a",
},
},
}
og2 := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1",
"kind": "OperatorGroup",
"metadata": map[string]any{
"name": "og-2",
"namespace": "ns-a",
},
},
}
og3 := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1",
"kind": "OperatorGroup",
"metadata": map[string]any{
"name": "og-3",
"namespace": "ns-b",
},
},
}
listKinds := map[schema.GroupVersionResource]string{
{Group: "operators.coreos.com", Version: "v1", Resource: "operatorgroups"}: "OperatorGroupList",
}
scheme := runtime.NewScheme()
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, og1, og2, og3)
a := common.Analyzer{
Context: context.TODO(),
Client: &kubernetes.Client{DynamicClient: dc},
}
res, err := (OperatorGroupAnalyzer{}).Analyze(a)
if err != nil {
t.Fatalf("Analyze error: %v", err)
}
if len(res) != 1 {
t.Fatalf("expected 1 result for ns-a overlap, got %d", len(res))
}
if res[0].Kind != "OperatorGroup" || res[0].Name != "ns-a" {
t.Fatalf("unexpected result: %#v", res[0])
}
}

View File

@@ -123,6 +123,20 @@ func analyzeContainerStatusFailures(a common.Analyzer, statuses []v1.ContainerSt
Sensitive: []common.Sensitive{},
})
}
} else if containerStatus.State.Terminated != nil {
if containerStatus.State.Terminated.ExitCode != 0 {
// This represents a container that is terminated abnormally
// https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-state-terminated
exitCode := containerStatus.State.Terminated.ExitCode
reason := containerStatus.State.Terminated.Reason
if reason == "" {
reason = "Unknown"
}
failures = append(failures, common.Failure{
Text: fmt.Sprintf("the termination reason is %s exitCode=%d container=%s pod=%s", reason, exitCode, containerStatus.Name, name),
Sensitive: []common.Sensitive{},
})
}
} else {
// when pod is Running but its ReadinessProbe fails
if !containerStatus.Ready && statusPhase == "Running" {

View File

@@ -343,6 +343,57 @@ func TestPodAnalyzer(t *testing.T) {
},
},
},
{
name: "Terminated container with non-zero exit code",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "Pod1",
Namespace: "default",
},
Status: v1.PodStatus{
Phase: v1.PodFailed,
ContainerStatuses: []v1.ContainerStatus{
{
Name: "Container1",
Ready: false,
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 1,
Reason: "Error",
},
},
},
{
Name: "Container2",
Ready: false,
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 2,
Reason: "",
},
},
},
},
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/Pod1",
failuresCount: 2,
},
},
},
}
podAnalyzer := PodAnalyzer{}

201
pkg/analyzer/security.go Normal file
View File

@@ -0,0 +1,201 @@
/*
Copyright 2024 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type SecurityAnalyzer struct{}
func (SecurityAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "Security"
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
"analyzer_name": kind,
})
var results []common.Result
// Analyze ServiceAccounts
saResults, err := analyzeServiceAccounts(a)
if err != nil {
return nil, err
}
results = append(results, saResults...)
// Analyze RoleBindings
rbResults, err := analyzeRoleBindings(a)
if err != nil {
return nil, err
}
results = append(results, rbResults...)
// Analyze Pod Security Contexts
podResults, err := analyzePodSecurityContexts(a)
if err != nil {
return nil, err
}
results = append(results, podResults...)
return results, nil
}
func analyzeServiceAccounts(a common.Analyzer) ([]common.Result, error) {
var results []common.Result
sas, err := a.Client.GetClient().CoreV1().ServiceAccounts(a.Namespace).List(a.Context, metav1.ListOptions{
LabelSelector: a.LabelSelector,
})
if err != nil {
return nil, err
}
for _, sa := range sas.Items {
var failures []common.Failure
// Check for default service account usage
if sa.Name == "default" {
pods, err := a.Client.GetClient().CoreV1().Pods(sa.Namespace).List(a.Context, metav1.ListOptions{})
if err != nil {
continue
}
defaultSAUsers := []string{}
for _, pod := range pods.Items {
if pod.Spec.ServiceAccountName == "default" {
defaultSAUsers = append(defaultSAUsers, pod.Name)
}
}
if len(defaultSAUsers) > 0 {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("Default service account is being used by pods: %v", defaultSAUsers),
Sensitive: []common.Sensitive{},
})
}
}
if len(failures) > 0 {
results = append(results, common.Result{
Kind: "Security/ServiceAccount",
Name: fmt.Sprintf("%s/%s", sa.Namespace, sa.Name),
Error: failures,
})
AnalyzerErrorsMetric.WithLabelValues("Security/ServiceAccount", sa.Name, sa.Namespace).Set(float64(len(failures)))
}
}
return results, nil
}
func analyzeRoleBindings(a common.Analyzer) ([]common.Result, error) {
var results []common.Result
rbs, err := a.Client.GetClient().RbacV1().RoleBindings(a.Namespace).List(a.Context, metav1.ListOptions{
LabelSelector: a.LabelSelector,
})
if err != nil {
return nil, err
}
for _, rb := range rbs.Items {
var failures []common.Failure
// Check for wildcards in role references
role, err := a.Client.GetClient().RbacV1().Roles(rb.Namespace).Get(a.Context, rb.RoleRef.Name, metav1.GetOptions{})
if err != nil {
continue
}
for _, rule := range role.Rules {
if containsWildcard(rule.Verbs) || containsWildcard(rule.Resources) {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("RoleBinding %s references Role %s which contains wildcard permissions - this is not recommended for security best practices", rb.Name, role.Name),
Sensitive: []common.Sensitive{},
})
}
}
if len(failures) > 0 {
results = append(results, common.Result{
Kind: "Security/RoleBinding",
Name: fmt.Sprintf("%s/%s", rb.Namespace, rb.Name),
Error: failures,
})
AnalyzerErrorsMetric.WithLabelValues("Security/RoleBinding", rb.Name, rb.Namespace).Set(float64(len(failures)))
}
}
return results, nil
}
func analyzePodSecurityContexts(a common.Analyzer) ([]common.Result, error) {
var results []common.Result
pods, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{
LabelSelector: a.LabelSelector,
})
if err != nil {
return nil, err
}
for _, pod := range pods.Items {
var failures []common.Failure
// Check for privileged containers first (most critical)
hasPrivilegedContainer := false
for _, container := range pod.Spec.Containers {
if container.SecurityContext != nil && container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("Container %s in pod %s is running as privileged which poses security risks", container.Name, pod.Name),
Sensitive: []common.Sensitive{},
})
hasPrivilegedContainer = true
break
}
}
// Only check for missing security context if no privileged containers found
if !hasPrivilegedContainer && pod.Spec.SecurityContext == nil {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("Pod %s does not have a security context defined which may pose security risks", pod.Name),
Sensitive: []common.Sensitive{},
})
}
if len(failures) > 0 {
results = append(results, common.Result{
Kind: "Security/Pod",
Name: fmt.Sprintf("%s/%s", pod.Namespace, pod.Name),
Error: failures[:1],
})
AnalyzerErrorsMetric.WithLabelValues("Security/Pod", pod.Name, pod.Namespace).Set(1)
}
}
return results, nil
}
func containsWildcard(slice []string) bool {
for _, item := range slice {
if item == "*" {
return true
}
}
return false
}

View File

@@ -0,0 +1,181 @@
/*
Copyright 2024 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"context"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestSecurityAnalyzer(t *testing.T) {
tests := []struct {
name string
namespace string
serviceAccounts []v1.ServiceAccount
pods []v1.Pod
roles []rbacv1.Role
roleBindings []rbacv1.RoleBinding
expectedErrors int
expectedKinds []string
}{
{
name: "default service account usage",
namespace: "default",
serviceAccounts: []v1.ServiceAccount{
{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "default",
},
},
},
pods: []v1.Pod{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "default",
},
Spec: v1.PodSpec{
ServiceAccountName: "default",
},
},
},
expectedErrors: 2,
expectedKinds: []string{"Security/ServiceAccount", "Security/Pod"},
},
{
name: "privileged container",
namespace: "default",
pods: []v1.Pod{
{
ObjectMeta: metav1.ObjectMeta{
Name: "privileged-pod",
Namespace: "default",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "privileged-container",
SecurityContext: &v1.SecurityContext{
Privileged: boolPtr(true),
},
},
},
},
},
},
expectedErrors: 1,
expectedKinds: []string{"Security/Pod"},
},
{
name: "wildcard permissions in role",
namespace: "default",
roles: []rbacv1.Role{
{
ObjectMeta: metav1.ObjectMeta{
Name: "wildcard-role",
Namespace: "default",
},
Rules: []rbacv1.PolicyRule{
{
Verbs: []string{"*"},
Resources: []string{"pods"},
},
},
},
},
roleBindings: []rbacv1.RoleBinding{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-binding",
Namespace: "default",
},
RoleRef: rbacv1.RoleRef{
Kind: "Role",
Name: "wildcard-role",
},
},
},
expectedErrors: 1,
expectedKinds: []string{"Security/RoleBinding"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := fake.NewSimpleClientset()
// Create test resources
for _, sa := range tt.serviceAccounts {
_, err := client.CoreV1().ServiceAccounts(tt.namespace).Create(context.TODO(), &sa, metav1.CreateOptions{})
assert.NoError(t, err)
}
for _, pod := range tt.pods {
_, err := client.CoreV1().Pods(tt.namespace).Create(context.TODO(), &pod, metav1.CreateOptions{})
assert.NoError(t, err)
}
for _, role := range tt.roles {
_, err := client.RbacV1().Roles(tt.namespace).Create(context.TODO(), &role, metav1.CreateOptions{})
assert.NoError(t, err)
}
for _, rb := range tt.roleBindings {
_, err := client.RbacV1().RoleBindings(tt.namespace).Create(context.TODO(), &rb, metav1.CreateOptions{})
assert.NoError(t, err)
}
analyzer := SecurityAnalyzer{}
results, err := analyzer.Analyze(common.Analyzer{
Client: &kubernetes.Client{Client: client},
Context: context.TODO(),
Namespace: tt.namespace,
})
assert.NoError(t, err)
// Debug: Print all results
t.Logf("Got %d results:", len(results))
for _, result := range results {
t.Logf(" Kind: %s, Name: %s", result.Kind, result.Name)
for _, failure := range result.Error {
t.Logf(" Failure: %s", failure.Text)
}
}
// Count results by kind
resultsByKind := make(map[string]int)
for _, result := range results {
resultsByKind[result.Kind]++
}
// Check that we have the expected number of results for each kind
for _, expectedKind := range tt.expectedKinds {
assert.Equal(t, 1, resultsByKind[expectedKind], "Expected 1 result of kind %s", expectedKind)
}
// Check total number of results matches expected kinds
assert.Equal(t, len(tt.expectedKinds), len(results), "Expected %d total results", len(tt.expectedKinds))
})
}
}

View File

@@ -24,145 +24,232 @@ import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/leaderelection/resourcelock"
)
func TestServiceAnalyzer(t *testing.T) {
config := common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "Endpoint1",
Namespace: "test",
},
// Endpoint with non-zero subsets.
Subsets: []v1.EndpointSubset{
{
// These not ready end points will contribute to failures.
NotReadyAddresses: []v1.EndpointAddress{
{
TargetRef: &v1.ObjectReference{
Kind: "test-reference",
Name: "reference1",
},
},
{
TargetRef: &v1.ObjectReference{
Kind: "test-reference",
Name: "reference2",
},
},
},
},
{
// These not ready end points will contribute to failures.
NotReadyAddresses: []v1.EndpointAddress{
{
TargetRef: &v1.ObjectReference{
Kind: "test-reference",
Name: "reference3",
},
},
},
},
},
},
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "Endpoint2",
Namespace: "test",
Annotations: map[string]string{
// Leader election record annotation key defined.
resourcelock.LeaderElectionRecordAnnotationKey: "this is okay",
},
},
// Endpoint with zero subsets.
},
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
// This won't contribute to any failures.
Name: "non-existent-service",
Namespace: "test",
Annotations: map[string]string{},
},
// Endpoint with zero subsets.
},
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "Service1",
Namespace: "test",
Annotations: map[string]string{},
},
// Endpoint with zero subsets.
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "Service1",
Namespace: "test",
},
Spec: v1.ServiceSpec{
Selector: map[string]string{
"app1": "test-app1",
"app2": "test-app2",
},
},
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
// This service won't be discovered.
Name: "Service2",
Namespace: "default",
},
Spec: v1.ServiceSpec{
Selector: map[string]string{
"app1": "test-app1",
"app2": "test-app2",
},
},
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "Service3",
Namespace: "test",
},
Spec: v1.ServiceSpec{
// No Spec Selector
},
},
),
},
Context: context.Background(),
Namespace: "test",
}
sAnalyzer := ServiceAnalyzer{}
results, err := sAnalyzer.Analyze(config)
require.NoError(t, err)
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
expectations := []struct {
name string
failuresCount int
tests := []struct {
name string
config common.Analyzer
expectations []struct {
name string
failuresCount int
}
}{
{
name: "test/Endpoint1",
failuresCount: 1,
name: "Service with no endpoints",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
Subsets: []v1.EndpointSubset{}, // Empty subsets
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
Spec: v1.ServiceSpec{
Selector: map[string]string{
"app": "test",
},
},
},
),
},
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/test-service",
failuresCount: 1, // One failure for no endpoints
},
},
},
{
name: "test/Service1",
failuresCount: 2,
name: "Service with not ready endpoints",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
Subsets: []v1.EndpointSubset{
{
NotReadyAddresses: []v1.EndpointAddress{
{
TargetRef: &v1.ObjectReference{
Kind: "Pod",
Name: "test-pod",
},
},
},
},
},
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
Spec: v1.ServiceSpec{
Selector: map[string]string{
"app": "test",
},
},
},
),
},
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/test-service",
failuresCount: 1, // One failure for not ready endpoints
},
},
},
{
name: "Service with warning events",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
Subsets: []v1.EndpointSubset{}, // Empty subsets
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
Spec: v1.ServiceSpec{
Selector: map[string]string{
"app": "test",
},
},
},
&v1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: "test-event",
Namespace: "default",
},
InvolvedObject: v1.ObjectReference{
Kind: "Service",
Name: "test-service",
Namespace: "default",
},
Type: "Warning",
Reason: "TestReason",
Message: "Test warning message",
},
),
},
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/test-service",
failuresCount: 2, // One failure for no endpoints, one for warning event
},
},
},
{
name: "Service with leader election annotation",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
Annotations: map[string]string{
"control-plane.alpha.kubernetes.io/leader": "test-leader",
},
},
Subsets: []v1.EndpointSubset{}, // Empty subsets
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
Spec: v1.ServiceSpec{
Selector: map[string]string{
"app": "test",
},
},
},
),
},
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
// No expectations for leader election endpoints
},
},
{
name: "Service with non-existent service",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
Subsets: []v1.EndpointSubset{}, // Empty subsets
},
),
},
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
// No expectations for non-existent service
},
},
}
require.Equal(t, len(expectations), len(results))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
analyzer := ServiceAnalyzer{}
results, err := analyzer.Analyze(tt.config)
require.NoError(t, err)
require.Len(t, results, len(tt.expectations))
for i, result := range results {
require.Equal(t, expectations[i].name, result.Name)
require.Equal(t, expectations[i].failuresCount, len(result.Error))
// Sort results by name for consistent comparison
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
for i, expectation := range tt.expectations {
require.Equal(t, expectation.name, results[i].Name)
require.Len(t, results[i].Error, expectation.failuresCount)
}
})
}
}

216
pkg/analyzer/storage.go Normal file
View File

@@ -0,0 +1,216 @@
/*
Copyright 2024 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type StorageAnalyzer struct{}
func (StorageAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "Storage"
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
"analyzer_name": kind,
})
var results []common.Result
// Analyze StorageClasses
scResults, err := analyzeStorageClasses(a)
if err != nil {
return nil, err
}
results = append(results, scResults...)
// Analyze PersistentVolumes
pvResults, err := analyzePersistentVolumes(a)
if err != nil {
return nil, err
}
results = append(results, pvResults...)
// Analyze PVCs with enhanced checks
pvcResults, err := analyzePersistentVolumeClaims(a)
if err != nil {
return nil, err
}
results = append(results, pvcResults...)
return results, nil
}
func analyzeStorageClasses(a common.Analyzer) ([]common.Result, error) {
var results []common.Result
scs, err := a.Client.GetClient().StorageV1().StorageClasses().List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
for _, sc := range scs.Items {
var failures []common.Failure
// Check for deprecated storage classes
if sc.Provisioner == "kubernetes.io/no-provisioner" {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("StorageClass %s uses deprecated provisioner 'kubernetes.io/no-provisioner'", sc.Name),
Sensitive: []common.Sensitive{},
})
}
// Check for default storage class
if sc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" {
// Check if there are multiple default storage classes
defaultCount := 0
for _, otherSc := range scs.Items {
if otherSc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" {
defaultCount++
}
}
if defaultCount > 1 {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("Multiple default StorageClasses found (%d), which can cause confusion", defaultCount),
Sensitive: []common.Sensitive{},
})
}
}
if len(failures) > 0 {
results = append(results, common.Result{
Kind: "Storage/StorageClass",
Name: sc.Name,
Error: failures,
})
AnalyzerErrorsMetric.WithLabelValues("Storage/StorageClass", sc.Name, "").Set(float64(len(failures)))
}
}
return results, nil
}
func analyzePersistentVolumes(a common.Analyzer) ([]common.Result, error) {
var results []common.Result
pvs, err := a.Client.GetClient().CoreV1().PersistentVolumes().List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
for _, pv := range pvs.Items {
var failures []common.Failure
// Check for released PVs
if pv.Status.Phase == v1.VolumeReleased {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("PersistentVolume %s is in Released state and should be cleaned up", pv.Name),
Sensitive: []common.Sensitive{},
})
}
// Check for failed PVs
if pv.Status.Phase == v1.VolumeFailed {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("PersistentVolume %s is in Failed state", pv.Name),
Sensitive: []common.Sensitive{},
})
}
// Check for small PVs (less than 1Gi)
if capacity, ok := pv.Spec.Capacity[v1.ResourceStorage]; ok {
if capacity.Cmp(resource.MustParse("1Gi")) < 0 {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("PersistentVolume %s has small capacity (%s)", pv.Name, capacity.String()),
Sensitive: []common.Sensitive{},
})
}
}
if len(failures) > 0 {
results = append(results, common.Result{
Kind: "Storage/PersistentVolume",
Name: pv.Name,
Error: failures,
})
AnalyzerErrorsMetric.WithLabelValues("Storage/PersistentVolume", pv.Name, "").Set(float64(len(failures)))
}
}
return results, nil
}
func analyzePersistentVolumeClaims(a common.Analyzer) ([]common.Result, error) {
var results []common.Result
pvcs, err := a.Client.GetClient().CoreV1().PersistentVolumeClaims(a.Namespace).List(a.Context, metav1.ListOptions{
LabelSelector: a.LabelSelector,
})
if err != nil {
return nil, err
}
for _, pvc := range pvcs.Items {
var failures []common.Failure
// Check for PVC state issues first (most critical)
switch pvc.Status.Phase {
case v1.ClaimPending:
failures = append(failures, common.Failure{
Text: fmt.Sprintf("PersistentVolumeClaim %s is in Pending state", pvc.Name),
Sensitive: []common.Sensitive{},
})
case v1.ClaimLost:
failures = append(failures, common.Failure{
Text: fmt.Sprintf("PersistentVolumeClaim %s is in Lost state", pvc.Name),
Sensitive: []common.Sensitive{},
})
default:
// Only check other issues if PVC is not in a critical state
if capacity, ok := pvc.Spec.Resources.Requests[v1.ResourceStorage]; ok {
if capacity.Cmp(resource.MustParse("1Gi")) < 0 {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("PersistentVolumeClaim %s has small capacity (%s)", pvc.Name, capacity.String()),
Sensitive: []common.Sensitive{},
})
}
}
// Check for missing storage class
if pvc.Spec.StorageClassName == nil && pvc.Spec.VolumeName == "" {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("PersistentVolumeClaim %s has no StorageClass specified", pvc.Name),
Sensitive: []common.Sensitive{},
})
}
}
// Only report the first failure found
if len(failures) > 0 {
results = append(results, common.Result{
Kind: "Storage/PersistentVolumeClaim",
Name: fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name),
Error: failures[:1],
})
AnalyzerErrorsMetric.WithLabelValues("Storage/PersistentVolumeClaim", pvc.Name, pvc.Namespace).Set(1)
}
}
return results, nil
}

View File

@@ -0,0 +1,254 @@
/*
Copyright 2024 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"context"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
v1 "k8s.io/api/core/v1"
storagev1 "k8s.io/api/storage/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestStorageAnalyzer(t *testing.T) {
tests := []struct {
name string
namespace string
storageClasses []storagev1.StorageClass
pvs []v1.PersistentVolume
pvcs []v1.PersistentVolumeClaim
expectedErrors int
}{
{
name: "Deprecated StorageClass",
namespace: "default",
storageClasses: []storagev1.StorageClass{
{
ObjectMeta: metav1.ObjectMeta{
Name: "deprecated-sc",
},
Provisioner: "kubernetes.io/no-provisioner",
},
},
expectedErrors: 1,
},
{
name: "Multiple Default StorageClasses",
namespace: "default",
storageClasses: []storagev1.StorageClass{
{
ObjectMeta: metav1.ObjectMeta{
Name: "default-sc1",
Annotations: map[string]string{
"storageclass.kubernetes.io/is-default-class": "true",
},
},
Provisioner: "kubernetes.io/gce-pd",
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "default-sc2",
Annotations: map[string]string{
"storageclass.kubernetes.io/is-default-class": "true",
},
},
Provisioner: "kubernetes.io/aws-ebs",
},
},
expectedErrors: 2,
},
{
name: "Released PV",
namespace: "default",
pvs: []v1.PersistentVolume{
{
ObjectMeta: metav1.ObjectMeta{
Name: "released-pv",
},
Status: v1.PersistentVolumeStatus{
Phase: v1.VolumeReleased,
},
},
},
expectedErrors: 1,
},
{
name: "Failed PV",
namespace: "default",
pvs: []v1.PersistentVolume{
{
ObjectMeta: metav1.ObjectMeta{
Name: "failed-pv",
},
Status: v1.PersistentVolumeStatus{
Phase: v1.VolumeFailed,
},
},
},
expectedErrors: 1,
},
{
name: "Small PV",
namespace: "default",
pvs: []v1.PersistentVolume{
{
ObjectMeta: metav1.ObjectMeta{
Name: "small-pv",
},
Spec: v1.PersistentVolumeSpec{
Capacity: v1.ResourceList{
v1.ResourceStorage: resource.MustParse("500Mi"),
},
},
},
},
expectedErrors: 1,
},
{
name: "Pending PVC",
namespace: "default",
pvcs: []v1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pending-pvc",
Namespace: "default",
},
Status: v1.PersistentVolumeClaimStatus{
Phase: v1.ClaimPending,
},
},
},
expectedErrors: 1,
},
{
name: "Lost PVC",
namespace: "default",
pvcs: []v1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "lost-pvc",
Namespace: "default",
},
Status: v1.PersistentVolumeClaimStatus{
Phase: v1.ClaimLost,
},
},
},
expectedErrors: 1,
},
{
name: "Small PVC",
namespace: "default",
pvcs: []v1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "small-pvc",
Namespace: "default",
},
Spec: v1.PersistentVolumeClaimSpec{
Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceStorage: resource.MustParse("500Mi"),
},
},
},
},
},
expectedErrors: 1,
},
{
name: "PVC without StorageClass",
namespace: "default",
pvcs: []v1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "no-sc-pvc",
Namespace: "default",
},
Spec: v1.PersistentVolumeClaimSpec{
Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
},
},
expectedErrors: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create fake client
client := fake.NewSimpleClientset()
// Create test resources
for _, sc := range tt.storageClasses {
_, err := client.StorageV1().StorageClasses().Create(context.TODO(), &sc, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create StorageClass: %v", err)
}
}
for _, pv := range tt.pvs {
_, err := client.CoreV1().PersistentVolumes().Create(context.TODO(), &pv, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create PV: %v", err)
}
}
for _, pvc := range tt.pvcs {
_, err := client.CoreV1().PersistentVolumeClaims(tt.namespace).Create(context.TODO(), &pvc, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create PVC: %v", err)
}
}
// Create analyzer
analyzer := StorageAnalyzer{}
// Create analyzer config
config := common.Analyzer{
Client: &kubernetes.Client{
Client: client,
},
Context: context.TODO(),
Namespace: tt.namespace,
}
// Run analysis
results, err := analyzer.Analyze(config)
if err != nil {
t.Fatalf("Failed to run analysis: %v", err)
}
// Count total errors
totalErrors := 0
for _, result := range results {
totalErrors += len(result.Error)
}
// Check error count
if totalErrors != tt.expectedErrors {
t.Errorf("Expected %d errors, got %d", tt.expectedErrors, totalErrors)
}
})
}
}

View File

@@ -0,0 +1,55 @@
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type SubscriptionAnalyzer struct{}
var subGVR = schema.GroupVersionResource{
Group: "operators.coreos.com", Version: "v1alpha1", Resource: "subscriptions",
}
func (SubscriptionAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
kind := "Subscription"
if a.Client.GetDynamicClient() == nil {
return nil, fmt.Errorf("dynamic client is nil in %s analyzer", kind)
}
list, err := a.Client.GetDynamicClient().
Resource(subGVR).Namespace(metav1.NamespaceAll).
List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
var results []common.Result
for _, item := range list.Items {
ns, name := item.GetNamespace(), item.GetName()
state, _, _ := unstructured.NestedString(item.Object, "status", "state")
conds, _, _ := unstructured.NestedSlice(item.Object, "status", "conditions")
var failures []common.Failure
if state == "" || state == "UpgradePending" || state == "UpgradeAvailable" {
msg := "subscription not at latest"
if c := pickWorstCondition(conds); c != "" {
msg += "; " + c
}
failures = append(failures, common.Failure{Text: fmt.Sprintf("state=%q: %s", state, msg)})
}
if len(failures) > 0 {
results = append(results, common.Result{
Kind: kind,
Name: ns + "/" + name,
Error: failures,
})
}
}
return results, nil
}

View File

@@ -0,0 +1,78 @@
package analyzer
import (
"context"
"strings"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
)
func TestSubscriptionAnalyzer(t *testing.T) {
ok := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1alpha1",
"kind": "Subscription",
"metadata": map[string]any{
"name": "ok-sub",
"namespace": "ns1",
},
"status": map[string]any{
"state": "AtLatestKnown",
},
},
}
bad := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "operators.coreos.com/v1alpha1",
"kind": "Subscription",
"metadata": map[string]any{
"name": "upgrade-sub",
"namespace": "ns1",
},
"status": map[string]any{
"state": "UpgradeAvailable",
"conditions": []interface{}{
map[string]any{
"status": "False",
"reason": "CatalogSourcesUnhealthy",
"message": "not reachable",
},
},
},
},
}
listKinds := map[schema.GroupVersionResource]string{
{Group: "operators.coreos.com", Version: "v1alpha1", Resource: "subscriptions"}: "SubscriptionList",
}
scheme := runtime.NewScheme()
dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, listKinds, ok, bad)
a := common.Analyzer{
Context: context.TODO(),
Client: &kubernetes.Client{DynamicClient: dc},
}
res, err := (SubscriptionAnalyzer{}).Analyze(a)
if err != nil {
t.Fatalf("Analyze error: %v", err)
}
if len(res) != 1 {
t.Fatalf("expected 1 result, got %d", len(res))
}
if res[0].Kind != "Subscription" || !strings.Contains(res[0].Name, "ns1/upgrade-sub") {
t.Fatalf("unexpected result: %#v", res[0])
}
if len(res[0].Error) == 0 || !strings.Contains(res[0].Error[0].Text, "CatalogSourcesUnhealthy") {
t.Fatalf("expected 'CatalogSourcesUnhealthy' in failure, got %#v", res[0].Error)
}
}

View File

@@ -0,0 +1,10 @@
package analyzer
// Helper functions for tests
func boolPtr(b bool) *bool {
return &b
}
func int64Ptr(i int64) *int64 {
return &i
}

View File

@@ -1,12 +1,15 @@
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"
"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)
@@ -59,6 +62,10 @@ func (c *InterplexCache) Store(key string, data string) error {
}
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 {
@@ -70,36 +77,52 @@ func (c *InterplexCache) Load(key string) (string, error) {
Key: key,
}
resp, err := c.cacheServiceClient.Get(context.Background(), &req)
// check if response is cache error not found
if err != nil {
return "", err
}
return resp.Value, nil
}
func (InterplexCache) List() ([]CacheObjectDetails, error) {
//TODO implement me
return nil, errors.New("not implemented")
func (c *InterplexCache) List() ([]CacheObjectDetails, error) {
// Not implemented for Interplex cache
return []CacheObjectDetails{}, nil
}
func (InterplexCache) Remove(key string) error {
func (c *InterplexCache) Remove(key string) error {
if os.Getenv("INTERPLEX_LOCAL_MODE") != "" {
c.configuration.ConnectionString = "localhost:8084"
}
return errors.New("not implemented")
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 {
if _, err := c.Load(key); err != nil {
return false
}
return true
_, err := c.Load(key)
return err == nil
}
func (c *InterplexCache) IsCacheDisabled() bool {
return c.noCache
}
func (InterplexCache) GetName() string {
//TODO implement me
func (c *InterplexCache) GetName() string {
return "interplex"
}

18
pkg/cache/s3_based.go vendored
View File

@@ -3,8 +3,9 @@ package cache
import (
"bytes"
"crypto/tls"
"log"
"errors"
"net/http"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
@@ -27,16 +28,19 @@ type S3CacheConfiguration struct {
func (s *S3Cache) Configure(cacheInfo CacheProvider) error {
if cacheInfo.S3.BucketName == "" {
log.Fatal("Bucket name not configured")
return errors.New("bucket name not configured")
}
s.bucketName = cacheInfo.S3.BucketName
sess := session.Must(session.NewSessionWithOptions(session.Options{
sess, err := session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
Config: aws.Config{
Region: aws.String(cacheInfo.S3.Region),
},
}))
})
if err != nil {
return errors.New("failed to create AWS session; please check your AWS credentials and configuration: " + err.Error())
}
if cacheInfo.S3.Endpoint != "" {
sess.Config.Endpoint = &cacheInfo.S3.Endpoint
sess.Config.S3ForcePathStyle = aws.Bool(true)
@@ -50,10 +54,14 @@ func (s *S3Cache) Configure(cacheInfo CacheProvider) error {
s3Client := s3.New(sess)
// Check if the bucket exists, if not create it
_, err := s3Client.HeadBucket(&s3.HeadBucketInput{
_, err = s3Client.HeadBucket(&s3.HeadBucketInput{
Bucket: aws.String(cacheInfo.S3.BucketName),
})
if err != nil {
// Check for AWS credentials error
if strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "SignatureDoesNotMatch") || strings.Contains(err.Error(), "NoCredentialProviders") {
return errors.New("aws credentials are invalid or missing; please check your AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or AWS config")
}
_, err = s3Client.CreateBucket(&s3.CreateBucketInput{
Bucket: aws.String(cacheInfo.S3.BucketName),
})

View File

@@ -28,6 +28,7 @@ import (
v1 "k8s.io/api/core/v1"
networkv1 "k8s.io/api/networking/v1"
policyv1 "k8s.io/api/policy/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gtwapi "sigs.k8s.io/gateway-api/apis/v1"
)
@@ -68,6 +69,8 @@ type PreAnalysis struct {
ScaledObject keda.ScaledObject
KyvernoPolicyReport kyverno.PolicyReport
KyvernoClusterPolicyReport kyverno.ClusterPolicyReport
Catalog ClusterCatalog
Extension ClusterExtension
}
type Result struct {
@@ -93,3 +96,117 @@ type Sensitive struct {
Unmasked string
Masked string
}
type (
SourceType string
AvailabilityMode string
UpgradeConstraintPolicy string
CRDUpgradeSafetyEnforcement string
)
type ClusterCatalog struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
Spec ClusterCatalogSpec `json:"spec"`
Status ClusterCatalogStatus `json:"status,omitempty"`
}
type ClusterCatalogList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []ClusterCatalog `json:"items"`
}
type ClusterCatalogSpec struct {
Source CatalogSource `json:"source"`
Priority int32 `json:"priority"`
AvailabilityMode AvailabilityMode `json:"availabilityMode,omitempty"`
}
type ClusterCatalogStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
ResolvedSource *ResolvedCatalogSource `json:"resolvedSource,omitempty"`
URLs *ClusterCatalogURLs `json:"urls,omitempty"`
LastUnpacked *metav1.Time `json:"lastUnpacked,omitempty"`
}
type ClusterCatalogURLs struct {
Base string `json:"base"`
}
type CatalogSource struct {
Type SourceType `json:"type"`
Image *ImageSource `json:"image,omitempty"`
}
type ResolvedCatalogSource struct {
Type SourceType `json:"type"`
Image *ResolvedImageSource `json:"image"`
}
type ResolvedImageSource struct {
Ref string `json:"ref"`
}
type ImageSource struct {
Ref string `json:"ref"`
PollIntervalMinutes *int `json:"pollIntervalMinutes,omitempty"`
}
type ClusterExtension struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ClusterExtensionSpec `json:"spec,omitempty"`
Status ClusterExtensionStatus `json:"status,omitempty"`
}
type ClusterExtensionSpec struct {
Namespace string `json:"namespace"`
ServiceAccount ServiceAccountReference `json:"serviceAccount"`
Source SourceConfig `json:"source"`
Install *ClusterExtensionInstallConfig `json:"install,omitempty"`
}
type ClusterExtensionInstallConfig struct {
Preflight *PreflightConfig `json:"preflight,omitempty"`
}
type PreflightConfig struct {
CRDUpgradeSafety *CRDUpgradeSafetyPreflightConfig `json:"crdUpgradeSafety"`
}
type CRDUpgradeSafetyPreflightConfig struct {
Enforcement CRDUpgradeSafetyEnforcement `json:"enforcement"`
}
type ServiceAccountReference struct {
Name string `json:"name"`
}
type SourceConfig struct {
SourceType string `json:"sourceType"`
Catalog *CatalogFilter `json:"catalog,omitempty"`
}
type CatalogFilter struct {
PackageName string `json:"packageName"`
Version string `json:"version,omitempty"`
Channels []string `json:"channels,omitempty"`
Selector *metav1.LabelSelector `json:"selector,omitempty"`
UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"`
}
type ClusterExtensionStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
Install *ClusterExtensionInstallStatus `json:"install,omitempty"`
}
type ClusterExtensionInstallStatus struct {
Bundle BundleMetadata `json:"bundle"`
}
type BundleMetadata struct {
Name string `json:"name"`
Version string `json:"version"`
}

View File

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

View File

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

View File

@@ -1,30 +1,49 @@
# serve
# K8sGPT MCP Server
The serve commands allow you to run k8sgpt in a grpc server mode.
This would be enabled typically through `k8sgpt serve` and is how the in-cluster k8sgpt deployment functions when managed by the [k8sgpt-operator](https://github.com/k8sgpt-ai/k8sgpt-operator)
This directory contains the implementation of the Mission Control Protocol (MCP) server for K8sGPT. The MCP server allows K8sGPT to be integrated with other tools that support the MCP protocol.
The grpc interface that is served is hosted on [buf](https://buf.build/k8sgpt-ai/schemas) and the repository for this is [here](https://github.com/k8sgpt-ai/schemas)
## Components
## grpcurl
- `mcp.go`: The main MCP server implementation
- `server.go`: The HTTP server implementation
- `tools.go`: Tool definitions for the MCP server
A fantastic tool for local debugging and development is `grpcurl`
It allows you to form curl like requests that are http2
e.g.
## Features
```
grpcurl -plaintext -d '{"namespace": "k8sgpt", "explain" : "true"}' localhost:8080 schema.v1.ServiceAnalyzeService/Analyze
```
The MCP server provides the following features:
```
grpcurl -plaintext localhost:8080 schema.v1.ServiceConfigService/ListIntegrations
{
"integrations": [
"prometheus"
]
1. **Analyze Kubernetes Resources**: Analyze Kubernetes resources in a cluster
2. **Get Cluster Information**: Retrieve information about the Kubernetes cluster
## Usage
To use the MCP server, you need to:
1. Initialize the MCP server with a Kubernetes client
2. Start the server
3. Connect to the server using an MCP client
Example:
```go
client, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatalf("Failed to create Kubernetes client: %v", err)
}
mcpServer := server.NewMCPServer(client)
if err := mcpServer.Start(); err != nil {
log.Fatalf("Failed to start MCP server: %v", err)
}
```
```
grpcurl -plaintext -d '{"integrations":{"prometheus":{"enabled":"true","namespace":"default","skipInstall":"false"}}}' localhost:8080 schema.v1.ServiceConfigService/AddConfig
```
## Integration
The MCP server can be integrated with other tools that support the MCP protocol, such as:
- Mission Control
- Other MCP-compatible tools
## License
This code is licensed under the Apache License 2.0.

View File

@@ -0,0 +1,60 @@
# K8sGPT MCP Client Example
This directory contains an example of how to use the K8sGPT MCP client in a real-world scenario.
## Prerequisites
- Go 1.16 or later
- Access to a Kubernetes cluster
- `kubectl` configured to access your cluster
## Building the Example
To build the example, run:
```bash
go build -o mcp-client-example
```
## Running the Example
To run the example, use the following command:
```bash
./mcp-client-example --kubeconfig=/path/to/kubeconfig --namespace=default
```
### Command-line Flags
- `--kubeconfig`: Path to the kubeconfig file (optional, defaults to the standard location)
- `--namespace`: Kubernetes namespace to analyze (optional)
## Example Output
When you run the example, you should see output similar to the following:
```
Starting MCP client...
```
The client will continue running until you press Ctrl+C to stop it.
## Integration with Mission Control
To integrate this example with Mission Control, you need to:
1. Start the MCP client using the example
2. Configure Mission Control to connect to the MCP client
3. Use Mission Control to analyze your Kubernetes cluster
## Troubleshooting
If you encounter any issues, check the following:
1. Ensure that your Kubernetes cluster is accessible
2. Verify that your kubeconfig file is valid
3. Check that the namespace you specified exists
## License
This code is licensed under the Apache License 2.0.

View File

@@ -0,0 +1,190 @@
/*
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 main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"time"
)
// AnalyzeRequest represents the input parameters for the analyze tool
type AnalyzeRequest struct {
Namespace string `json:"namespace,omitempty"`
Backend string `json:"backend,omitempty"`
Language string `json:"language,omitempty"`
Filters []string `json:"filters,omitempty"`
LabelSelector string `json:"labelSelector,omitempty"`
NoCache bool `json:"noCache,omitempty"`
Explain bool `json:"explain,omitempty"`
MaxConcurrency int `json:"maxConcurrency,omitempty"`
WithDoc bool `json:"withDoc,omitempty"`
InteractiveMode bool `json:"interactiveMode,omitempty"`
CustomHeaders []string `json:"customHeaders,omitempty"`
WithStats bool `json:"withStats,omitempty"`
}
// JSONRPCResponse represents the JSON-RPC response format
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID int `json:"id"`
Result struct {
Content []struct {
Text string `json:"text"`
Type string `json:"type"`
} `json:"content"`
} `json:"result,omitempty"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error,omitempty"`
}
func main() {
// Parse command line flags
serverPort := flag.String("port", "8089", "Port of the MCP server")
namespace := flag.String("namespace", "", "Kubernetes namespace to analyze")
backend := flag.String("backend", "", "AI backend to use")
language := flag.String("language", "english", "Language for analysis")
flag.Parse()
// Create analyze request
req := AnalyzeRequest{
Namespace: *namespace,
Backend: *backend,
Language: *language,
Explain: true,
MaxConcurrency: 10,
}
// Note: req is now used directly in the JSON-RPC request
// Create HTTP client with timeout
client := &http.Client{
Timeout: 5 * time.Minute,
}
// First, initialize the session
initRequest := map[string]interface{}{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": map[string]interface{}{
"protocolVersion": "2025-03-26",
"capabilities": map[string]interface{}{
"tools": map[string]interface{}{},
"resources": map[string]interface{}{},
"prompts": map[string]interface{}{},
},
"clientInfo": map[string]interface{}{
"name": "k8sgpt-client",
"version": "1.0.0",
},
},
}
initData, err := json.Marshal(initRequest)
if err != nil {
log.Fatalf("Failed to marshal init request: %v", err)
}
// Send initialization request
initResp, err := client.Post(
fmt.Sprintf("http://localhost:%s/mcp", *serverPort),
"application/json",
bytes.NewBuffer(initData),
)
if err != nil {
log.Fatalf("Failed to send init request: %v", err)
}
defer func() {
if err := initResp.Body.Close(); err != nil {
log.Printf("Error closing init response body: %v", err)
}
}()
// Extract session ID from response headers
sessionID := initResp.Header.Get("Mcp-Session-Id")
if sessionID == "" {
log.Println("Warning: No session ID received from server")
}
// Create JSON-RPC request for analyze
jsonRPCRequest := map[string]interface{}{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": map[string]interface{}{
"name": "analyze",
"arguments": req,
},
}
// Convert to JSON
jsonRPCData, err := json.Marshal(jsonRPCRequest)
if err != nil {
log.Fatalf("Failed to marshal JSON-RPC request: %v", err)
}
// Create request with session ID if available
httpReq, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%s/mcp", *serverPort), bytes.NewBuffer(jsonRPCData))
if err != nil {
log.Fatalf("Failed to create request: %v", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json,text/event-stream")
if sessionID != "" {
httpReq.Header.Set("Mcp-Session-Id", sessionID)
}
// Send request to MCP server
resp, err := client.Do(httpReq)
if err != nil {
log.Fatalf("Failed to send request: %v", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Printf("Error closing response body: %v", err)
}
}()
// Read and print raw response for debugging
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Failed to read response body: %v", err)
}
fmt.Printf("Raw response: %s\n", string(body))
// Parse response
var jsonRPCResp JSONRPCResponse
if err := json.Unmarshal(body, &jsonRPCResp); err != nil {
log.Fatalf("Failed to decode response: %v", err)
}
// Print results
fmt.Println("Analysis Results:")
if jsonRPCResp.Error != nil {
fmt.Printf("Error: %s (code: %d)\n", jsonRPCResp.Error.Message, jsonRPCResp.Error.Code)
} else if len(jsonRPCResp.Result.Content) > 0 {
fmt.Println(jsonRPCResp.Result.Content[0].Text)
} else {
fmt.Println("No results returned")
}
}

View File

@@ -1,8 +1,9 @@
package config
import (
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"context"
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
"github.com/k8sgpt-ai/k8sgpt/pkg/custom"
"github.com/spf13/viper"
@@ -20,19 +21,13 @@ const (
notUsedInsecure = false
)
func (h *Handler) AddConfig(ctx context.Context, i *schemav1.AddConfigRequest) (*schemav1.AddConfigResponse, error,
) {
resp, err := h.syncIntegration(ctx, i)
if err != nil {
return resp, err
}
// ApplyConfig applies the configuration changes from the request
func (h *Handler) ApplyConfig(ctx context.Context, i *schemav1.AddConfigRequest) error {
if i.CustomAnalyzers != nil {
// We need to add the custom analyzers to the viper config and save them
var customAnalyzers = make([]custom.CustomAnalyzer, 0)
if err := viper.UnmarshalKey("custom_analyzers", &customAnalyzers); err != nil {
return resp, err
return err
} else {
// If there are analyzers are already in the config we will append the ones with new names
for _, ca := range i.CustomAnalyzers {
@@ -56,7 +51,7 @@ func (h *Handler) AddConfig(ctx context.Context, i *schemav1.AddConfigRequest) (
// save the config
viper.Set("custom_analyzers", customAnalyzers)
if err := viper.WriteConfig(); err != nil {
return resp, err
return err
}
}
}
@@ -74,18 +69,30 @@ func (h *Handler) AddConfig(ctx context.Context, i *schemav1.AddConfigRequest) (
case *schemav1.Cache_InterplexCache:
remoteCache, err = cache.NewCacheProvider("interplex", notUsedBucket, notUsedRegion, i.Cache.GetInterplexCache().Endpoint, notUsedStorageAcc, notUsedContainerName, notUsedProjectId, notUsedInsecure)
default:
return resp, status.Error(codes.InvalidArgument, "Invalid cache configuration")
return status.Error(codes.InvalidArgument, "Invalid cache configuration")
}
if err != nil {
return resp, err
return err
}
err = cache.AddRemoteCache(remoteCache)
if err != nil {
return resp, err
return err
}
}
return nil
}
func (h *Handler) AddConfig(ctx context.Context, i *schemav1.AddConfigRequest) (*schemav1.AddConfigResponse, error) {
resp, err := h.syncIntegration(ctx, i)
if err != nil {
return resp, err
}
if err := h.ApplyConfig(ctx, i); err != nil {
return resp, err
}
return resp, nil
}

View File

@@ -1,18 +1,15 @@
package config
import (
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"context"
"fmt"
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"github.com/k8sgpt-ai/k8sgpt/pkg/integration"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
//const (
// trivyName = "trivy"
//)
// syncIntegration is aware of the following events
// A new integration added
// An integration removed from the Integration block

View File

@@ -0,0 +1,74 @@
/*
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 main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
"github.com/k8sgpt-ai/k8sgpt/pkg/server"
"go.uber.org/zap"
)
func main() {
// Parse command line flags
port := flag.String("port", "8089", "Port to run the MCP server on")
useHTTP := flag.Bool("http", false, "Enable HTTP mode for MCP server")
flag.Parse()
// Initialize zap logger
logger, err := zap.NewProduction()
if err != nil {
log.Fatalf("Error creating logger: %v", err)
}
defer func() {
if err := logger.Sync(); err != nil {
log.Printf("Error syncing logger: %v", err)
}
}()
// Create AI provider
aiProvider := &ai.AIProvider{
Name: "openai",
Password: os.Getenv("OPENAI_API_KEY"),
Model: "gpt-3.5-turbo",
}
// Create and start MCP server
mcpServer, err := server.NewMCPServer(*port, aiProvider, *useHTTP, logger)
if err != nil {
log.Fatalf("Error creating MCP server: %v", err)
}
// Start the server in a goroutine
go func() {
if err := mcpServer.Start(); err != nil {
log.Fatalf("Error starting MCP server: %v", err)
}
}()
// Handle graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
// Cleanup
if err := mcpServer.Close(); err != nil {
log.Printf("Error closing MCP server: %v", err)
}
}

530
pkg/server/mcp.go Normal file
View File

@@ -0,0 +1,530 @@
/*
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 server
import (
"context"
"encoding/json"
"fmt"
"regexp"
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/k8sgpt-ai/k8sgpt/pkg/server/config"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/spf13/viper"
"go.uber.org/zap"
)
// K8sGptMCPServer represents an MCP server for k8sgpt
type K8sGptMCPServer struct {
server *server.MCPServer
port string
aiProvider *ai.AIProvider
useHTTP bool
logger *zap.Logger
httpServer *server.StreamableHTTPServer
stdioServer *server.StdioServer
}
func NewMCPServer(port string, aiProvider *ai.AIProvider, useHTTP bool, logger *zap.Logger) (*K8sGptMCPServer, error) {
opts := []server.ServerOption{
server.WithToolCapabilities(true),
server.WithResourceCapabilities(true, false),
server.WithPromptCapabilities(false),
}
// Create the MCP server
mcpServer := server.NewMCPServer("k8sgpt", "1.0.0", opts...)
var k8sGptMCPServer = &K8sGptMCPServer{
server: mcpServer,
port: port,
aiProvider: aiProvider,
useHTTP: useHTTP,
logger: logger,
}
// Register tools and resources immediately
if err := k8sGptMCPServer.registerToolsAndResources(); err != nil {
return nil, fmt.Errorf("failed to register tools and resources: %v", err)
}
if useHTTP {
// Create HTTP server with streamable transport
httpOpts := []server.StreamableHTTPOption{
server.WithLogger(&zapLoggerAdapter{logger: logger}),
}
httpServer := server.NewStreamableHTTPServer(mcpServer, httpOpts...)
// Launch the HTTP server directly
go func() {
logger.Info("Starting MCP HTTP server", zap.String("port", port))
if err := httpServer.Start(":" + port); err != nil {
logger.Fatal("MCP HTTP server failed", zap.Error(err))
}
}()
return &K8sGptMCPServer{
server: mcpServer,
port: port,
aiProvider: aiProvider,
useHTTP: useHTTP,
logger: logger,
httpServer: httpServer,
}, nil
} else {
// Create stdio server
stdioServer := server.NewStdioServer(mcpServer)
return &K8sGptMCPServer{
server: mcpServer,
port: port,
aiProvider: aiProvider,
useHTTP: useHTTP,
logger: logger,
stdioServer: stdioServer,
}, nil
}
}
// Start starts the MCP server
func (s *K8sGptMCPServer) Start() error {
if s.server == nil {
return fmt.Errorf("server not initialized")
}
// Register prompts
if err := s.registerPrompts(); err != nil {
return fmt.Errorf("failed to register prompts: %v", err)
}
// Register resources
if err := s.registerResources(); err != nil {
return fmt.Errorf("failed to register resources: %v", err)
}
// Start the server based on transport type
if s.useHTTP {
// HTTP server is already running in a goroutine
return nil
} else {
// Start stdio server (this will block)
return server.ServeStdio(s.server)
}
}
func (s *K8sGptMCPServer) registerToolsAndResources() error {
// Register analyze tool with proper JSON schema
analyzeTool := mcp.NewTool("analyze",
mcp.WithDescription("Analyze Kubernetes resources for issues and problems"),
mcp.WithString("namespace",
mcp.Description("Kubernetes namespace to analyze (empty for all namespaces)"),
),
mcp.WithString("backend",
mcp.Description("AI backend to use for analysis (e.g., openai, azure, localai)"),
),
mcp.WithBoolean("explain",
mcp.Description("Provide detailed explanations for issues"),
),
mcp.WithArray("filters",
mcp.Description("Provide filters to narrow down the analysis (e.g. ['Pods', 'Deployments'])"),
// without below line MCP server fails with Google Agent Development Kit (ADK), interestingly works fine with mcpinspector
mcp.WithStringItems(),
),
)
s.server.AddTool(analyzeTool, s.handleAnalyze)
// Register cluster info tool (no parameters needed)
clusterInfoTool := mcp.NewTool("cluster-info",
mcp.WithDescription("Get Kubernetes cluster information and version"),
)
s.server.AddTool(clusterInfoTool, s.handleClusterInfo)
// Register config tool with proper JSON schema
configTool := mcp.NewTool("config",
mcp.WithDescription("Configure K8sGPT settings including custom analyzers and cache"),
mcp.WithObject("customAnalyzers",
mcp.Description("Custom analyzer configurations"),
mcp.Properties(map[string]any{
"name": map[string]any{
"type": "string",
"description": "Name of the custom analyzer",
},
"connection": map[string]any{
"type": "object",
"properties": map[string]any{
"url": map[string]any{
"type": "string",
"description": "URL of the custom analyzer service",
},
"port": map[string]any{
"type": "integer",
"description": "Port of the custom analyzer service",
},
},
},
}),
),
mcp.WithObject("cache",
mcp.Description("Cache configuration"),
mcp.Properties(map[string]any{
"type": map[string]any{
"type": "string",
"description": "Cache type (s3, azure, gcs)",
"enum": []string{"s3", "azure", "gcs"},
},
"bucketName": map[string]any{
"type": "string",
"description": "Bucket name for S3/GCS cache",
},
"region": map[string]any{
"type": "string",
"description": "Region for S3/GCS cache",
},
"endpoint": map[string]any{
"type": "string",
"description": "Custom endpoint for S3 cache",
},
"insecure": map[string]any{
"type": "boolean",
"description": "Use insecure connection for cache",
},
"storageAccount": map[string]any{
"type": "string",
"description": "Storage account for Azure cache",
},
"containerName": map[string]any{
"type": "string",
"description": "Container name for Azure cache",
},
"projectId": map[string]any{
"type": "string",
"description": "Project ID for GCS cache",
},
}),
),
)
s.server.AddTool(configTool, s.handleConfig)
return nil
}
// AnalyzeRequest represents the input parameters for the analyze tool
type AnalyzeRequest struct {
Namespace string `json:"namespace,omitempty"`
Backend string `json:"backend,omitempty"`
Language string `json:"language,omitempty"`
Filters []string `json:"filters,omitempty"`
LabelSelector string `json:"labelSelector,omitempty"`
NoCache bool `json:"noCache,omitempty"`
Explain bool `json:"explain,omitempty"`
MaxConcurrency int `json:"maxConcurrency,omitempty"`
WithDoc bool `json:"withDoc,omitempty"`
InteractiveMode bool `json:"interactiveMode,omitempty"`
CustomHeaders []string `json:"customHeaders,omitempty"`
WithStats bool `json:"withStats,omitempty"`
Anonymize bool `json:"anonymize,omitempty"`
}
// AnalyzeResponse represents the output of the analyze tool
type AnalyzeResponse struct {
Results string `json:"results"`
}
// ClusterInfoRequest represents the input parameters for the cluster-info tool
type ClusterInfoRequest struct {
// Empty struct as we don't need any input parameters
}
// ClusterInfoResponse represents the output of the cluster-info tool
type ClusterInfoResponse struct {
Info string `json:"info"`
}
// ConfigRequest represents the input parameters for the config tool
type ConfigRequest struct {
CustomAnalyzers []struct {
Name string `json:"name"`
Connection struct {
Url string `json:"url"`
Port int `json:"port"`
} `json:"connection"`
} `json:"customAnalyzers,omitempty"`
Cache struct {
Type string `json:"type"`
// S3 specific fields
BucketName string `json:"bucketName,omitempty"`
Region string `json:"region,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Insecure bool `json:"insecure,omitempty"`
// Azure specific fields
StorageAccount string `json:"storageAccount,omitempty"`
ContainerName string `json:"containerName,omitempty"`
// GCS specific fields
ProjectId string `json:"projectId,omitempty"`
} `json:"cache,omitempty"`
}
// ConfigResponse represents the output of the config tool
type ConfigResponse struct {
Status string `json:"status"`
}
// handleAnalyze handles the analyze tool
func (s *K8sGptMCPServer) handleAnalyze(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var req AnalyzeRequest
if err := request.BindArguments(&req); err != nil {
return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil
}
if req.Backend == "" {
if s.aiProvider.Name != "" {
req.Backend = s.aiProvider.Name
} else {
req.Backend = "openai" // fallback default
}
}
// Get stored filters if not specified
if len(req.Filters) == 0 {
req.Filters = viper.GetStringSlice("active_filters")
}
// Validate MaxConcurrency to prevent excessive memory allocation
req.MaxConcurrency = validateMaxConcurrency(req.MaxConcurrency)
// Create a new analysis with the request parameters
analysis, err := analysis.NewAnalysis(
req.Backend,
req.Language,
req.Filters,
req.Namespace,
req.LabelSelector,
req.NoCache,
req.Explain,
req.MaxConcurrency,
req.WithDoc,
req.InteractiveMode,
req.CustomHeaders,
req.WithStats,
)
if err != nil {
return mcp.NewToolResultErrorf("Failed to create analysis: %v", err), nil
}
defer analysis.Close()
// Run the analysis
analysis.RunAnalysis()
if req.Explain {
var output string
err := analysis.GetAIResults(output, req.Anonymize)
if err != nil {
return mcp.NewToolResultErrorf("Failed to get results from AI: %v", err), nil
}
// Convert results to JSON string using PrintOutput
outputBytes, err := analysis.PrintOutput("text")
if err != nil {
return mcp.NewToolResultErrorf("Failed to convert results to string: %v", err), nil
}
plainText := stripANSI(string(outputBytes))
return mcp.NewToolResultText(plainText), nil
} else {
// Get the output
output, err := analysis.PrintOutput("json")
if err != nil {
return mcp.NewToolResultErrorf("Failed to print output: %v", err), nil
}
return mcp.NewToolResultText(string(output)), nil
}
}
// validateMaxConcurrency validates and bounds the MaxConcurrency parameter
func validateMaxConcurrency(maxConcurrency int) int {
const maxAllowedConcurrency = 100
if maxConcurrency <= 0 {
return 10 // Default value if not set
} else if maxConcurrency > maxAllowedConcurrency {
return maxAllowedConcurrency // Cap at a reasonable maximum
}
return maxConcurrency
}
// handleClusterInfo handles the cluster-info tool
func (s *K8sGptMCPServer) handleClusterInfo(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Create a new Kubernetes client
client, err := kubernetes.NewClient("", "")
if err != nil {
return mcp.NewToolResultErrorf("failed to create Kubernetes client: %v", err), nil
}
// Get cluster info from the client
version, err := client.Client.Discovery().ServerVersion()
if err != nil {
return mcp.NewToolResultErrorf("failed to get cluster version: %v", err), nil
}
info := fmt.Sprintf("Kubernetes %s", version.GitVersion)
return mcp.NewToolResultText(info), nil
}
// handleConfig handles the config tool
func (s *K8sGptMCPServer) handleConfig(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Parse request arguments
var req ConfigRequest
if err := request.BindArguments(&req); err != nil {
return mcp.NewToolResultErrorf("Failed to parse request arguments: %v", err), nil
}
// Create a new config handler
handler := &config.Handler{}
// Convert request to AddConfigRequest
addConfigReq := &schemav1.AddConfigRequest{
CustomAnalyzers: make([]*schemav1.CustomAnalyzer, 0),
}
// Add custom analyzers if present
if len(req.CustomAnalyzers) > 0 {
for _, ca := range req.CustomAnalyzers {
addConfigReq.CustomAnalyzers = append(addConfigReq.CustomAnalyzers, &schemav1.CustomAnalyzer{
Name: ca.Name,
Connection: &schemav1.Connection{
Url: ca.Connection.Url,
Port: fmt.Sprintf("%d", ca.Connection.Port),
},
})
}
}
// Add cache configuration if present
if req.Cache.Type != "" {
cacheConfig := &schemav1.Cache{}
switch req.Cache.Type {
case "s3":
cacheConfig.CacheType = &schemav1.Cache_S3Cache{
S3Cache: &schemav1.S3Cache{
BucketName: req.Cache.BucketName,
Region: req.Cache.Region,
Endpoint: req.Cache.Endpoint,
Insecure: req.Cache.Insecure,
},
}
case "azure":
cacheConfig.CacheType = &schemav1.Cache_AzureCache{
AzureCache: &schemav1.AzureCache{
StorageAccount: req.Cache.StorageAccount,
ContainerName: req.Cache.ContainerName,
},
}
case "gcs":
cacheConfig.CacheType = &schemav1.Cache_GcsCache{
GcsCache: &schemav1.GCSCache{
BucketName: req.Cache.BucketName,
Region: req.Cache.Region,
ProjectId: req.Cache.ProjectId,
},
}
}
addConfigReq.Cache = cacheConfig
}
// Apply the configuration using the shared function
if err := handler.ApplyConfig(ctx, addConfigReq); err != nil {
return mcp.NewToolResultErrorf("Failed to add config: %v", err), nil
}
return mcp.NewToolResultText("Successfully added configuration"), nil
}
// registerPrompts registers the prompts for the MCP server
func (s *K8sGptMCPServer) registerPrompts() error {
// Register any prompts needed for the MCP server
return nil
}
// registerResources registers the resources for the MCP server
func (s *K8sGptMCPServer) registerResources() error {
clusterInfoResource := mcp.NewResource("cluster-info", "cluster-info",
mcp.WithResourceDescription("Get information about the Kubernetes cluster"),
mcp.WithMIMEType("application/json"),
)
s.server.AddResource(clusterInfoResource, s.getClusterInfo)
return nil
}
func (s *K8sGptMCPServer) getClusterInfo(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
// Create a new Kubernetes client
client, err := kubernetes.NewClient("", "")
if err != nil {
return nil, fmt.Errorf("failed to create Kubernetes client: %v", err)
}
// Get cluster info from the client
version, err := client.Client.Discovery().ServerVersion()
if err != nil {
return nil, fmt.Errorf("failed to get cluster version: %v", err)
}
data, err := json.Marshal(map[string]string{
"version": version.String(),
"platform": version.Platform,
"gitVersion": version.GitVersion,
})
if err != nil {
return []mcp.ResourceContents{
&mcp.TextResourceContents{
URI: "cluster-info",
MIMEType: "text/plain",
Text: "Failed to marshal cluster info",
},
}, nil
}
return []mcp.ResourceContents{
&mcp.TextResourceContents{
URI: "cluster-info",
MIMEType: "application/json",
Text: string(data),
},
}, nil
}
// Close closes the MCP server and releases resources
func (s *K8sGptMCPServer) Close() error {
return nil
}
// zapLoggerAdapter adapts zap.Logger to the interface expected by mark3labs/mcp-go
type zapLoggerAdapter struct {
logger *zap.Logger
}
func (z *zapLoggerAdapter) Infof(format string, v ...any) {
z.logger.Info(fmt.Sprintf(format, v...))
}
func (z *zapLoggerAdapter) Errorf(format string, v ...any) {
z.logger.Error(fmt.Sprintf(format, v...))
}
// stripANSI removes ANSI escape sequences from a string
func stripANSI(input string) string {
re := regexp.MustCompile(`\x1b\[[0-9;]*m`)
return re.ReplaceAllString(input, "")
}

View File

@@ -1,8 +1,10 @@
package query
import (
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"context"
"fmt"
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
)
@@ -10,9 +12,50 @@ func (h *Handler) Query(ctx context.Context, i *schemav1.QueryRequest) (
*schemav1.QueryResponse,
error,
) {
aiClient := ai.NewClient(i.Backend)
// Create client factory and config provider
factory := ai.GetAIClientFactory()
configProvider := ai.GetConfigProvider()
// Use the factory to create the client
aiClient := factory.NewClient(i.Backend)
defer aiClient.Close()
var configAI ai.AIConfiguration
if err := configProvider.UnmarshalKey("ai", &configAI); err != nil {
return &schemav1.QueryResponse{
Response: "",
Error: &schemav1.QueryError{
Message: fmt.Sprintf("Failed to unmarshal AI configuration: %v", err),
},
}, nil
}
var aiProvider ai.AIProvider
for _, provider := range configAI.Providers {
if i.Backend == provider.Name {
aiProvider = provider
break
}
}
if aiProvider.Name == "" {
return &schemav1.QueryResponse{
Response: "",
Error: &schemav1.QueryError{
Message: fmt.Sprintf("AI provider %s not found in configuration", i.Backend),
},
}, nil
}
// Configure the AI client
if err := aiClient.Configure(&aiProvider); err != nil {
return &schemav1.QueryResponse{
Response: "",
Error: &schemav1.QueryError{
Message: fmt.Sprintf("Failed to configure AI client: %v", err),
},
}, nil
}
resp, err := aiClient.GetCompletion(ctx, i.Query)
var errMessage string = ""
if err != nil {

View File

@@ -0,0 +1,310 @@
package query
import (
"context"
"errors"
"testing"
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockAI is a mock implementation of the ai.IAI interface for testing
type MockAI struct {
mock.Mock
}
func (m *MockAI) Configure(config ai.IAIConfig) error {
args := m.Called(config)
return args.Error(0)
}
func (m *MockAI) GetCompletion(ctx context.Context, prompt string) (string, error) {
args := m.Called(ctx, prompt)
return args.String(0), args.Error(1)
}
func (m *MockAI) GetName() string {
args := m.Called()
return args.String(0)
}
func (m *MockAI) Close() {
m.Called()
}
// MockAIClientFactory is a mock implementation of AIClientFactory
type MockAIClientFactory struct {
mock.Mock
}
func (m *MockAIClientFactory) NewClient(provider string) ai.IAI {
args := m.Called(provider)
return args.Get(0).(ai.IAI)
}
// MockConfigProvider is a mock implementation of ConfigProvider
type MockConfigProvider struct {
mock.Mock
}
func (m *MockConfigProvider) UnmarshalKey(key string, rawVal interface{}) error {
args := m.Called(key, rawVal)
// If we want to set the rawVal (which is a pointer)
if fn, ok := args.Get(0).(func(interface{})); ok && fn != nil {
fn(rawVal)
}
// Return the error as the first return value
return args.Error(0)
}
func TestQuery_Success(t *testing.T) {
// Setup mocks
mockAI := new(MockAI)
mockFactory := new(MockAIClientFactory)
mockConfig := new(MockConfigProvider)
// Set test implementations
ai.SetTestAIClientFactory(mockFactory)
ai.SetTestConfigProvider(mockConfig)
defer ai.ResetTestImplementations()
// Define test data
testBackend := "test-backend"
testQuery := "test query"
testResponse := "test response"
// Setup expectations
mockFactory.On("NewClient", testBackend).Return(mockAI)
mockAI.On("Close").Return()
// Set up configuration with a valid provider
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) {
config := args.Get(1).(*ai.AIConfiguration)
*config = ai.AIConfiguration{
Providers: []ai.AIProvider{
{
Name: testBackend,
Password: "test-password",
Model: "test-model",
},
},
}
}).Return(nil)
mockAI.On("Configure", mock.AnythingOfType("*ai.AIProvider")).Return(nil)
mockAI.On("GetCompletion", mock.Anything, testQuery).Return(testResponse, nil)
// Create handler and call Query
handler := &Handler{}
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
Backend: testBackend,
Query: testQuery,
})
// Assertions
assert.NoError(t, err)
assert.NotNil(t, response)
assert.Equal(t, testResponse, response.Response)
assert.Equal(t, "", response.Error.Message)
// Verify mocks
mockAI.AssertExpectations(t)
mockFactory.AssertExpectations(t)
mockConfig.AssertExpectations(t)
}
func TestQuery_UnmarshalError(t *testing.T) {
// Setup mocks
mockAI := new(MockAI)
mockFactory := new(MockAIClientFactory)
mockConfig := new(MockConfigProvider)
// Set test implementations
ai.SetTestAIClientFactory(mockFactory)
ai.SetTestConfigProvider(mockConfig)
defer ai.ResetTestImplementations()
// Setup expectations
mockFactory.On("NewClient", "test-backend").Return(mockAI)
mockAI.On("Close").Return()
// Mock unmarshal error
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Return(errors.New("unmarshal error"))
// Create handler and call Query
handler := &Handler{}
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
Backend: "test-backend",
Query: "test query",
})
// Assertions
assert.NoError(t, err)
assert.NotNil(t, response)
assert.Equal(t, "", response.Response)
assert.Contains(t, response.Error.Message, "Failed to unmarshal AI configuration")
// Verify mocks
mockAI.AssertExpectations(t)
mockFactory.AssertExpectations(t)
mockConfig.AssertExpectations(t)
}
func TestQuery_ProviderNotFound(t *testing.T) {
// Setup mocks
mockAI := new(MockAI)
mockFactory := new(MockAIClientFactory)
mockConfig := new(MockConfigProvider)
// Set test implementations
ai.SetTestAIClientFactory(mockFactory)
ai.SetTestConfigProvider(mockConfig)
defer ai.ResetTestImplementations()
// Define test data
testBackend := "test-backend"
// Setup expectations
mockFactory.On("NewClient", testBackend).Return(mockAI)
mockAI.On("Close").Return()
// Set up configuration with no matching provider
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) {
config := args.Get(1).(*ai.AIConfiguration)
*config = ai.AIConfiguration{
Providers: []ai.AIProvider{
{
Name: "other-backend",
},
},
}
}).Return(nil)
// Create handler and call Query
handler := &Handler{}
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
Backend: testBackend,
Query: "test query",
})
// Assertions
assert.NoError(t, err)
assert.NotNil(t, response)
assert.Equal(t, "", response.Response)
assert.Contains(t, response.Error.Message, "AI provider test-backend not found in configuration")
// Verify mocks
mockAI.AssertExpectations(t)
mockFactory.AssertExpectations(t)
mockConfig.AssertExpectations(t)
}
func TestQuery_ConfigureError(t *testing.T) {
// Setup mocks
mockAI := new(MockAI)
mockFactory := new(MockAIClientFactory)
mockConfig := new(MockConfigProvider)
// Set test implementations
ai.SetTestAIClientFactory(mockFactory)
ai.SetTestConfigProvider(mockConfig)
defer ai.ResetTestImplementations()
// Define test data
testBackend := "test-backend"
// Setup expectations
mockFactory.On("NewClient", testBackend).Return(mockAI)
mockAI.On("Close").Return()
// Set up configuration with a valid provider
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) {
config := args.Get(1).(*ai.AIConfiguration)
*config = ai.AIConfiguration{
Providers: []ai.AIProvider{
{
Name: testBackend,
},
},
}
}).Return(nil)
// Mock configure error
mockAI.On("Configure", mock.AnythingOfType("*ai.AIProvider")).Return(errors.New("configure error"))
// Create handler and call Query
handler := &Handler{}
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
Backend: testBackend,
Query: "test query",
})
// Assertions
assert.NoError(t, err)
assert.NotNil(t, response)
assert.Equal(t, "", response.Response)
assert.Contains(t, response.Error.Message, "Failed to configure AI client")
// Verify mocks
mockAI.AssertExpectations(t)
mockFactory.AssertExpectations(t)
mockConfig.AssertExpectations(t)
}
func TestQuery_GetCompletionError(t *testing.T) {
// Setup mocks
mockAI := new(MockAI)
mockFactory := new(MockAIClientFactory)
mockConfig := new(MockConfigProvider)
// Set test implementations
ai.SetTestAIClientFactory(mockFactory)
ai.SetTestConfigProvider(mockConfig)
defer ai.ResetTestImplementations()
// Define test data
testBackend := "test-backend"
testQuery := "test query"
// Setup expectations
mockFactory.On("NewClient", testBackend).Return(mockAI)
mockAI.On("Close").Return()
// Set up configuration with a valid provider
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) {
config := args.Get(1).(*ai.AIConfiguration)
*config = ai.AIConfiguration{
Providers: []ai.AIProvider{
{
Name: testBackend,
},
},
}
}).Return(nil)
mockAI.On("Configure", mock.AnythingOfType("*ai.AIProvider")).Return(nil)
mockAI.On("GetCompletion", mock.Anything, testQuery).Return("", errors.New("completion error"))
// Create handler and call Query
handler := &Handler{}
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
Backend: testBackend,
Query: testQuery,
})
// Assertions
assert.NoError(t, err)
assert.NotNil(t, response)
assert.Equal(t, "", response.Response)
assert.Equal(t, "completion error", response.Error.Message)
// Verify mocks
mockAI.AssertExpectations(t)
mockFactory.AssertExpectations(t)
mockConfig.AssertExpectations(t)
}

View File

@@ -1,11 +1,15 @@
package server
import (
"bytes"
"context"
"io"
"net"
"net/http"
"testing"
"time"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
"google.golang.org/grpc"
@@ -14,7 +18,12 @@ import (
func TestServe(t *testing.T) {
logger, _ := zap.NewDevelopment()
defer logger.Sync()
defer func() {
err := logger.Sync()
if err != nil {
t.Logf("logger.Sync() error: %v", err)
}
}()
s := &Config{
Port: "50059",
@@ -34,7 +43,11 @@ func TestServe(t *testing.T) {
conn, err := grpc.Dial("localhost:50059", grpc.WithInsecure())
assert.NoError(t, err, "Should be able to dial the server")
defer conn.Close()
defer func() {
if err := conn.Close(); err != nil {
t.Logf("failed to close connection: %v", err)
}
}()
// Test a simple gRPC reflection request
cli := grpc_reflection_v1alpha.NewServerReflectionClient(conn)
@@ -49,12 +62,278 @@ func TestServe(t *testing.T) {
assert.NoError(t, err, "Shutdown should not return an error")
}
// TestMCPServerCreation tests the creation of an MCP server
func TestMCPServerCreation(t *testing.T) {
logger, _ := zap.NewDevelopment()
defer func() {
err := logger.Sync()
if err != nil {
t.Logf("logger.Sync() error: %v", err)
}
}()
aiProvider := &ai.AIProvider{
Name: "test-provider",
Password: "test-password",
Model: "test-model",
}
// Test HTTP mode
mcpServer, err := NewMCPServer("8088", aiProvider, true, logger)
assert.NoError(t, err, "Should be able to create MCP server with HTTP transport")
assert.NotNil(t, mcpServer, "MCP server should not be nil")
assert.True(t, mcpServer.useHTTP, "MCP server should be in HTTP mode")
assert.Equal(t, "8088", mcpServer.port, "Port should be set correctly")
// Test stdio mode
mcpServerStdio, err := NewMCPServer("8088", aiProvider, false, logger)
assert.NoError(t, err, "Should be able to create MCP server with stdio transport")
assert.NotNil(t, mcpServerStdio, "MCP server should not be nil")
assert.False(t, mcpServerStdio.useHTTP, "MCP server should be in stdio mode")
}
// TestMCPServerBasicHTTP tests basic HTTP connectivity to the MCP server
func TestMCPServerBasicHTTP(t *testing.T) {
logger, _ := zap.NewDevelopment()
defer func() {
err := logger.Sync()
if err != nil {
t.Logf("logger.Sync() error: %v", err)
}
}()
aiProvider := &ai.AIProvider{
Name: "test-provider",
Password: "test-password",
Model: "test-model",
}
mcpServer, err := NewMCPServer("8091", aiProvider, true, logger)
assert.NoError(t, err, "Should be able to create MCP server")
// For HTTP mode, the server is already started in NewMCPServer
// No need to call Start() as it's already running in a goroutine
// Wait for the server to start
err = waitForPort("localhost:8091", 10*time.Second)
if err != nil {
t.Skipf("MCP server did not start within timeout: %v", err)
}
// First, initialize the session
initRequest := `{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
}
}`
initResp, err := http.Post("http://localhost:8091/mcp", "application/json", bytes.NewBufferString(initRequest))
if err != nil {
t.Logf("Initialize request failed: %v", err)
return
}
defer func() {
if err := initResp.Body.Close(); err != nil {
t.Logf("Error closing init response body: %v", err)
}
}()
// Read initialization response
initBody, err := io.ReadAll(initResp.Body)
if err != nil {
t.Logf("Failed to read init response body: %v", err)
} else {
t.Logf("Init response status: %d, body: %s", initResp.StatusCode, string(initBody))
}
// Extract session ID from response headers if present
sessionID := initResp.Header.Get("Mcp-Session-Id")
if sessionID == "" {
t.Logf("No session ID in response headers")
}
// Now test tools/list with session ID if available
headers := map[string]string{
"Content-Type": "application/json",
"Accept": "application/json,text/event-stream",
}
if sessionID != "" {
headers["Mcp-Session-Id"] = sessionID
}
req, err := http.NewRequest("POST", "http://localhost:8091/mcp", bytes.NewBufferString(`{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`))
if err != nil {
t.Logf("Failed to create request: %v", err)
return
}
for key, value := range headers {
req.Header.Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Logf("MCP endpoint test skipped (server might not be fully ready): %v", err)
return
}
defer func() {
err := resp.Body.Close()
if err != nil {
t.Logf("resp.Body.Close() error: %v", err)
}
}()
// Read response body for debugging
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Logf("Failed to read response body: %v", err)
} else {
t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body))
}
// Accept both 200 and 404 as valid responses (404 means endpoint not implemented)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
t.Errorf("MCP endpoint returned unexpected status: %d", resp.StatusCode)
}
// Cleanup
err = mcpServer.Close()
assert.NoError(t, err, "MCP server should close without error")
}
// TestMCPServerToolCall tests calling a specific tool (analyze) through the MCP server
func TestMCPServerToolCall(t *testing.T) {
logger, _ := zap.NewDevelopment()
defer func() {
err := logger.Sync()
if err != nil {
t.Logf("logger.Sync() error: %v", err)
}
}()
aiProvider := &ai.AIProvider{
Name: "test-provider",
Password: "test-password",
Model: "test-model",
}
mcpServer, err := NewMCPServer("8090", aiProvider, true, logger)
assert.NoError(t, err, "Should be able to create MCP server")
// For HTTP mode, the server is already started in NewMCPServer
// No need to call Start() as it's already running in a goroutine
// Wait for the server to start
err = waitForPort("localhost:8090", 10*time.Second)
if err != nil {
t.Skipf("MCP server did not start within timeout: %v", err)
}
// First, initialize the session
initRequest := `{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
}
}`
initResp, err := http.Post("http://localhost:8090/mcp", "application/json", bytes.NewBufferString(initRequest))
if err != nil {
t.Logf("Initialize request failed: %v", err)
return
}
defer func() {
if err := initResp.Body.Close(); err != nil {
t.Logf("Error closing init response body: %v", err)
}
}()
// Extract session ID from response headers if present
sessionID := initResp.Header.Get("Mcp-Session-Id")
// Test calling the analyze tool with proper JSON-RPC format
analyzeRequest := `{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "analyze",
"arguments": {
"namespace": "default",
"backend": "openai",
"language": "english",
"explain": true,
"maxConcurrency": 10
}
}
}`
// Create request with session ID if available
req, err := http.NewRequest("POST", "http://localhost:8090/mcp", bytes.NewBufferString(analyzeRequest))
if err != nil {
t.Logf("Failed to create request: %v", err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json,text/event-stream")
if sessionID != "" {
req.Header.Set("Mcp-Session-Id", sessionID)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Logf("Analyze tool call test skipped (server might not be fully ready): %v", err)
return
}
defer func() {
err := resp.Body.Close()
if err != nil {
t.Logf("resp.Body.Close() error: %v", err)
}
}()
// Accept both 200 and 404 as valid responses
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
t.Errorf("Analyze tool call returned unexpected status: %d", resp.StatusCode)
}
// Cleanup
err = mcpServer.Close()
assert.NoError(t, err, "MCP server should close without error")
}
func waitForPort(address string, timeout time.Duration) error {
start := time.Now()
for {
conn, err := net.Dial("tcp", address)
if err == nil {
conn.Close()
_ = conn.Close()
return nil
}
if time.Since(start) > timeout {

View File

@@ -14,6 +14,7 @@ limitations under the License.
package util
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
@@ -311,3 +312,33 @@ func LabelStrToSelector(labelStr string) labels.Selector {
}
return labels.SelectorFromSet(labels.Set(labelSelectorMap))
}
// CaptureOutput captures the output of a function that writes to stdout
func CaptureOutput(f func()) string {
old := os.Stdout
r, w, err := os.Pipe()
if err != nil {
panic(fmt.Sprintf("failed to create pipe: %v", err))
}
os.Stdout = w
// Ensure os.Stdout is restored even if panic occurs
defer func() {
os.Stdout = old
}()
f()
if err := w.Close(); err != nil {
panic(fmt.Sprintf("failed to close writer: %v", err))
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(r); err != nil {
panic(fmt.Sprintf("failed to read from pipe: %v", err))
}
return buf.String()
}
// Contains checks if substr is present in s
func Contains(s, substr string) bool {
return bytes.Contains([]byte(s), []byte(substr))
}