mirror of
https://github.com/k8sgpt-ai/k8sgpt.git
synced 2026-03-19 03:23:47 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0efe6f5c | ||
|
|
f9621af7e4 | ||
|
|
6052a5b4d7 | ||
|
|
42437f77d1 | ||
|
|
523362765f | ||
|
|
1459dd4b8e | ||
|
|
1b86a6fc89 | ||
|
|
5cf4fc52da | ||
|
|
cd049c9b4b | ||
|
|
86ebc23de7 | ||
|
|
a236cc4f28 | ||
|
|
6eac58d4b0 | ||
|
|
2994c1c5a7 | ||
|
|
fa4a0757b8 | ||
|
|
49e120c28e | ||
|
|
6479cbaf91 | ||
|
|
36995fd4ed | ||
|
|
fe450eb69d | ||
|
|
edda743fa2 | ||
|
|
57281df53c | ||
|
|
1f767ebd2e | ||
|
|
b7dc384547 | ||
|
|
c588e963de | ||
|
|
d13b91301c | ||
|
|
948dae5e28 | ||
|
|
c659a875fc | ||
|
|
f0d3f36f6d | ||
|
|
940a50e851 | ||
|
|
06542b4bf1 | ||
|
|
af826d500f | ||
|
|
cbe2fb4a4c | ||
|
|
032576c728 | ||
|
|
3099909113 | ||
|
|
097c7912b0 | ||
|
|
493c684eb7 | ||
|
|
a1f98ad78e | ||
|
|
e66de8c4ce | ||
|
|
aafe669739 | ||
|
|
e5e613acee | ||
|
|
ed73485d92 | ||
|
|
50916f2c93 | ||
|
|
c1410d1699 | ||
|
|
c5b72eee16 | ||
|
|
8cfb717dc1 | ||
|
|
639aa12931 | ||
|
|
123b8a66ee | ||
|
|
5d4e591f11 | ||
|
|
9f494fa884 | ||
|
|
9998e7620d | ||
|
|
b6b06123db | ||
|
|
9192b26fab | ||
|
|
65fff11e58 | ||
|
|
00aaae86d8 | ||
|
|
d6bcb96105 | ||
|
|
045a06350b |
2
.github/workflows/golangci_lint.yaml
vendored
2
.github/workflows/golangci_lint.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
|
||||
- name: golangci-lint
|
||||
uses: reviewdog/action-golangci-lint@f5d85915708b6e687c44764987a6912d390b17f6 # v2
|
||||
uses: reviewdog/action-golangci-lint@79d32f10b2ea0d4cebb755d849b048c4b40c3d50 # v2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-pr-check
|
||||
|
||||
8
.github/workflows/release.yaml
vendored
8
.github/workflows/release.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
|
||||
- uses: google-github-actions/release-please-action@c078ea33917ab8cfa5300e48f4b7e6b16606aede # v3
|
||||
- uses: google-github-actions/release-please-action@51ee8ae2605bd5ce1cfdcc5938684908f1cd9f69 # v3
|
||||
id: release
|
||||
with:
|
||||
command: manifest
|
||||
@@ -45,11 +45,11 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4
|
||||
with:
|
||||
go-version: '1.20'
|
||||
- name: Download Syft
|
||||
uses: anchore/sbom-action/download-syft@422cb34a0f8b599678c41b21163ea6088edb2624 # v0.14.1
|
||||
uses: anchore/sbom-action/download-syft@4d571ad1038a9cc29d676154ef265ab8f9027042 # v0.14.2
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b # v4
|
||||
with:
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
cache-to: type=gha,scope=${{ github.ref_name }}-${{ env.IMAGE_TAG }}
|
||||
|
||||
- name: Generate SBOM
|
||||
uses: anchore/sbom-action@422cb34a0f8b599678c41b21163ea6088edb2624 # v0.14.1
|
||||
uses: anchore/sbom-action@4d571ad1038a9cc29d676154ef265ab8f9027042 # v0.14.2
|
||||
with:
|
||||
image: ${{ env.IMAGE_TAG }}
|
||||
artifact-name: sbom-${{ env.IMAGE_NAME }}
|
||||
|
||||
10
.github/workflows/test.yaml
vendored
10
.github/workflows/test.yaml
vendored
@@ -19,9 +19,13 @@ jobs:
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4
|
||||
uses: actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753 # v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
- name: Unit Test
|
||||
run: make test
|
||||
|
||||
# - name: Fmt Test
|
||||
# run: fmtFiles=$(make fmt); if [ "$fmtFiles" != "" ];then exit 1; fi
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ builds:
|
||||
- darwin
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}}
|
||||
- -s -w -X main.commit={{.ShortCommit}}
|
||||
- -s -w -X main.Date={{.CommitDate}}
|
||||
|
||||
nfpms:
|
||||
- file_name_template: '{{ .ProjectName }}_{{ .Arch }}'
|
||||
|
||||
@@ -1 +1 @@
|
||||
{".":"0.2.9"}
|
||||
{".":"0.3.6"}
|
||||
142
CHANGELOG.md
142
CHANGELOG.md
@@ -1,5 +1,147 @@
|
||||
# Changelog
|
||||
|
||||
## [0.3.6](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.5...v0.3.6) (2023-05-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* get official field doc ([#457](https://github.com/k8sgpt-ai/k8sgpt/issues/457)) ([f9621af](https://github.com/k8sgpt-ai/k8sgpt/commit/f9621af7e480f490710020b931cbb08fb9824740))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.270 ([#465](https://github.com/k8sgpt-ai/k8sgpt/issues/465)) ([5cf4fc5](https://github.com/k8sgpt-ai/k8sgpt/commit/5cf4fc52da4542a8bae98764d2fa7e337d95e5bd))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.271 ([#469](https://github.com/k8sgpt-ai/k8sgpt/issues/469)) ([1459dd4](https://github.com/k8sgpt-ai/k8sgpt/commit/1459dd4b8eca937e95ebe9b727311dc8b023e304))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.272 ([#473](https://github.com/k8sgpt-ai/k8sgpt/issues/473)) ([5233627](https://github.com/k8sgpt-ai/k8sgpt/commit/523362765f4c064c02798bb9e6f31e2bcc856e5f))
|
||||
* **deps:** update module github.com/spf13/viper to v1.16.0 ([#472](https://github.com/k8sgpt-ai/k8sgpt/issues/472)) ([6052a5b](https://github.com/k8sgpt-ai/k8sgpt/commit/6052a5b4d77902e1882e3121b678671c89b57af8))
|
||||
* **deps:** update module github.com/stretchr/testify to v1.8.4 ([#471](https://github.com/k8sgpt-ai/k8sgpt/issues/471)) ([42437f7](https://github.com/k8sgpt-ai/k8sgpt/commit/42437f77d1e0735a8f38a62ddbefb4d1f4e61c0e))
|
||||
* name of sa reference in deployment ([#468](https://github.com/k8sgpt-ai/k8sgpt/issues/468)) ([cd049c9](https://github.com/k8sgpt-ai/k8sgpt/commit/cd049c9b4b188f702608d989fb32ae62f333dac5))
|
||||
* typo ([#463](https://github.com/k8sgpt-ai/k8sgpt/issues/463)) ([1b86a6f](https://github.com/k8sgpt-ai/k8sgpt/commit/1b86a6fc89f90d29fdf2fab87a517f0da225ec96))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update google-github-actions/release-please-action digest to 51ee8ae ([#464](https://github.com/k8sgpt-ai/k8sgpt/issues/464)) ([86ebc23](https://github.com/k8sgpt-ai/k8sgpt/commit/86ebc23de762583b5904605f5651bbc83760aa95))
|
||||
|
||||
## [0.3.5](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.4...v0.3.5) (2023-05-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add configuration api route ([#459](https://github.com/k8sgpt-ai/k8sgpt/issues/459)) ([fa4a075](https://github.com/k8sgpt-ai/k8sgpt/commit/fa4a0757b83f8ec00df951d49316f10961daa0e0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.267 ([#451](https://github.com/k8sgpt-ai/k8sgpt/issues/451)) ([49e120c](https://github.com/k8sgpt-ai/k8sgpt/commit/49e120c28e8b5ce5a8f7255ebc0f1b1b5c423f95))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.269 ([#458](https://github.com/k8sgpt-ai/k8sgpt/issues/458)) ([2994c1c](https://github.com/k8sgpt-ai/k8sgpt/commit/2994c1c5a77ce6ebe6e59d6edc9647c02f06f261))
|
||||
* updated list.go to handle k8sgpt cache list crashing issue ([#455](https://github.com/k8sgpt-ai/k8sgpt/issues/455)) ([6eac58d](https://github.com/k8sgpt-ai/k8sgpt/commit/6eac58d4b03169356d3f06674ef206472e149fde))
|
||||
|
||||
## [0.3.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.3...v0.3.4) (2023-05-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.266 ([#446](https://github.com/k8sgpt-ai/k8sgpt/issues/446)) ([edda743](https://github.com/k8sgpt-ai/k8sgpt/commit/edda743fa2bf4b5ae2551c981447a5912a459bb4))
|
||||
* **deps:** update module github.com/stretchr/testify to v1.8.3 ([#442](https://github.com/k8sgpt-ai/k8sgpt/issues/442)) ([fe450eb](https://github.com/k8sgpt-ai/k8sgpt/commit/fe450eb69da0645328e60e2d7b0852ffdb996dee))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* add more filter releavent UT in analysis_test.go ([#435](https://github.com/k8sgpt-ai/k8sgpt/issues/435)) ([36995fd](https://github.com/k8sgpt-ai/k8sgpt/commit/36995fd4ed41c8c83ebc308f8ec2a4bfe530f7dc))
|
||||
|
||||
## [0.3.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.2...v0.3.3) (2023-05-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* caching ([#439](https://github.com/k8sgpt-ai/k8sgpt/issues/439)) ([948dae5](https://github.com/k8sgpt-ai/k8sgpt/commit/948dae5e288ec3bb0165eb3ce32171b12003f9c7))
|
||||
* rework auth commands ([#438](https://github.com/k8sgpt-ai/k8sgpt/issues/438)) ([c659a87](https://github.com/k8sgpt-ai/k8sgpt/commit/c659a875fc296849a3703bfd7f0c4796f44bdf5a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* append coreAnalyzer if active_filter is empty and integration is added ([#441](https://github.com/k8sgpt-ai/k8sgpt/issues/441)) ([b7dc384](https://github.com/k8sgpt-ai/k8sgpt/commit/b7dc3845476759bedb3a55f77c8779e4a9f460dd))
|
||||
* **deps:** update kubernetes packages to v0.27.2 ([#436](https://github.com/k8sgpt-ai/k8sgpt/issues/436)) ([d13b913](https://github.com/k8sgpt-ai/k8sgpt/commit/d13b91301cab5e05349b68716cd506fa1705f36f))
|
||||
* **deps:** update module github.com/aws/aws-sdk-go to v1.44.265 ([#445](https://github.com/k8sgpt-ai/k8sgpt/issues/445)) ([c588e96](https://github.com/k8sgpt-ai/k8sgpt/commit/c588e963de3f238f07200d6cf09fe6f9781484f5))
|
||||
* docker version ([#444](https://github.com/k8sgpt-ai/k8sgpt/issues/444)) ([1f767eb](https://github.com/k8sgpt-ai/k8sgpt/commit/1f767ebd2e31e61decab36218b1b85f2b3b6207d))
|
||||
* use coreAnalyzer if there are no filters selected and no active_filters ([#432](https://github.com/k8sgpt-ai/k8sgpt/issues/432)) ([f0d3f36](https://github.com/k8sgpt-ai/k8sgpt/commit/f0d3f36f6d56bd76248590c0b841dffb7769a2ee))
|
||||
|
||||
## [0.3.2](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.1...v0.3.2) (2023-05-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added the ability to set a user default AI provider ([#427](https://github.com/k8sgpt-ai/k8sgpt/issues/427)) ([cbe2fb4](https://github.com/k8sgpt-ai/k8sgpt/commit/cbe2fb4a4c160a0a24b3fb4602cae8e5eebd6aa0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve default_prompt ([#406](https://github.com/k8sgpt-ai/k8sgpt/issues/406)) ([06542b4](https://github.com/k8sgpt-ai/k8sgpt/commit/06542b4bf1aec193f11a40526a1b60be01972e66))
|
||||
* missing validation for backend option in remove command ([#429](https://github.com/k8sgpt-ai/k8sgpt/issues/429)) ([af826d5](https://github.com/k8sgpt-ai/k8sgpt/commit/af826d500fef0469b958250161b0827aa4ab2933))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** bump github.com/docker/distribution ([#428](https://github.com/k8sgpt-ai/k8sgpt/issues/428)) ([3099909](https://github.com/k8sgpt-ai/k8sgpt/commit/30999091136c64173e5c15b789036c85f8b855f3))
|
||||
* **deps:** update actions/setup-go digest to fac708d ([#422](https://github.com/k8sgpt-ai/k8sgpt/issues/422)) ([097c791](https://github.com/k8sgpt-ai/k8sgpt/commit/097c7912b0572d0461f08af12e9f21c6618c13e4))
|
||||
* **deps:** update reviewdog/action-golangci-lint digest to 79d32f1 ([#425](https://github.com/k8sgpt-ai/k8sgpt/issues/425)) ([032576c](https://github.com/k8sgpt-ai/k8sgpt/commit/032576c728751522fe6cd8f255366cc3d5c0f23c))
|
||||
|
||||
## [0.3.1](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.3.0...v0.3.1) (2023-05-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add error message if analyze request fail ([#393](https://github.com/k8sgpt-ai/k8sgpt/issues/393)) ([639aa12](https://github.com/k8sgpt-ai/k8sgpt/commit/639aa12931b3fc9f99a4b34f33f583b9f427f40c))
|
||||
* add remove command to remove a backend AI provider ([#395](https://github.com/k8sgpt-ai/k8sgpt/issues/395)) ([c5b72ee](https://github.com/k8sgpt-ai/k8sgpt/commit/c5b72eee165a29d4ed59b0ec2f19e9f58267840d))
|
||||
* filters api ([#407](https://github.com/k8sgpt-ai/k8sgpt/issues/407)) ([e5e613a](https://github.com/k8sgpt-ai/k8sgpt/commit/e5e613acee0a99f108dd90affc55a18dee42ad9c))
|
||||
* use correct port to metrics ([#390](https://github.com/k8sgpt-ai/k8sgpt/issues/390)) ([5d4e591](https://github.com/k8sgpt-ai/k8sgpt/commit/5d4e591f11e555cac851205fff158ef22f702c33))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* clusterole name ([#392](https://github.com/k8sgpt-ai/k8sgpt/issues/392)) ([123b8a6](https://github.com/k8sgpt-ai/k8sgpt/commit/123b8a66eed1af41b7bd4e558ba4f0f8ef947e98))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.3.0-20230515081240-6b5b845c638e.1 ([#397](https://github.com/k8sgpt-ai/k8sgpt/issues/397)) ([a1f98ad](https://github.com/k8sgpt-ai/k8sgpt/commit/a1f98ad78ecd9a84d26e7a59c340e5410524e561))
|
||||
* **deps:** update module buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go to v1.28.1-20230510140658-54288a50e81c.4 ([#398](https://github.com/k8sgpt-ai/k8sgpt/issues/398)) ([50916f2](https://github.com/k8sgpt-ai/k8sgpt/commit/50916f2c93fb19b935f835eda1ba78b7c0465fe6))
|
||||
* **deps:** update module google.golang.org/grpc to v1.55.0 ([#389](https://github.com/k8sgpt-ai/k8sgpt/issues/389)) ([8cfb717](https://github.com/k8sgpt-ai/k8sgpt/commit/8cfb717dc15a6af489a16282d38d3495518bc919))
|
||||
* **deps:** update module helm.sh/helm/v3 to v3.12.0 ([#396](https://github.com/k8sgpt-ai/k8sgpt/issues/396)) ([c1410d1](https://github.com/k8sgpt-ai/k8sgpt/commit/c1410d169945341e9a635e12c2adcc87a08f8a09))
|
||||
* update engine's cmd flag to match the new cli layout ([#400](https://github.com/k8sgpt-ai/k8sgpt/issues/400)) ([aafe669](https://github.com/k8sgpt-ai/k8sgpt/commit/aafe669739aa8c38611d13deb08706096c7893e0))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* gofmt fix and enable in CI ([#414](https://github.com/k8sgpt-ai/k8sgpt/issues/414)) ([e66de8c](https://github.com/k8sgpt-ai/k8sgpt/commit/e66de8c4cea1213cda1db609f07cbb0c8f6591c3))
|
||||
* make go-lint happy ([#405](https://github.com/k8sgpt-ai/k8sgpt/issues/405)) ([ed73485](https://github.com/k8sgpt-ai/k8sgpt/commit/ed73485d92af0329915633d51c7eccdbce9b37cc))
|
||||
|
||||
## [0.3.0](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.9...v0.3.0) (2023-05-09)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* migrate api to grpc ([#386](https://github.com/k8sgpt-ai/k8sgpt/issues/386))
|
||||
|
||||
### Features
|
||||
|
||||
* add auth commands ([#369](https://github.com/k8sgpt-ai/k8sgpt/issues/369)) ([00aaae8](https://github.com/k8sgpt-ai/k8sgpt/commit/00aaae86d88812bd6b6be290ba440ea583ccc01c))
|
||||
* migrate api to grpc ([#386](https://github.com/k8sgpt-ai/k8sgpt/issues/386)) ([9998e76](https://github.com/k8sgpt-ai/k8sgpt/commit/9998e7620d2803b82b241482649449507040add3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update module github.com/sashabaranov/go-openai to v1.9.3 ([#378](https://github.com/k8sgpt-ai/k8sgpt/issues/378)) ([045a063](https://github.com/k8sgpt-ai/k8sgpt/commit/045a06350bf41d4177e67316978af8fcf02ff19a))
|
||||
* **deps:** update module golang.org/x/term to v0.8.0 ([#382](https://github.com/k8sgpt-ai/k8sgpt/issues/382)) ([65fff11](https://github.com/k8sgpt-ai/k8sgpt/commit/65fff11e585f8074fb77124b25339a09da313970))
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
* update README ([#383](https://github.com/k8sgpt-ai/k8sgpt/issues/383)) ([d6bcb96](https://github.com/k8sgpt-ai/k8sgpt/commit/d6bcb96105a549eb772b790704c7ec27e374eafd))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* **deps:** update anchore/sbom-action action to v0.14.2 ([#387](https://github.com/k8sgpt-ai/k8sgpt/issues/387)) ([9192b26](https://github.com/k8sgpt-ai/k8sgpt/commit/9192b26fab2ce09c8a480256a15723ae788612c3))
|
||||
* fix the logo URL ([#384](https://github.com/k8sgpt-ai/k8sgpt/issues/384)) ([b6b0612](https://github.com/k8sgpt-ai/k8sgpt/commit/b6b06123db8914cae09dfa96edd92aff83823bdf))
|
||||
|
||||
## [0.2.9](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.8...v0.2.9) (2023-05-03)
|
||||
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -54,7 +54,7 @@ all: tidy add-copyright lint cover build
|
||||
build:
|
||||
@echo "$(shell go version)"
|
||||
@echo "===========> Building binary $(BUILDAPP) *[Git Info]: $(VERSION)-$(GIT_COMMIT)"
|
||||
@export CGO_ENABLED=0 && go build -o $(BUILDAPP) -ldflags '-s -w' $(BUILDFILE)
|
||||
@export CGO_ENABLED=0 && go build -o $(BUILDAPP) -ldflags "-s -w -X main.version=dev -X main.commit=$$(git rev-parse --short HEAD) -X main.date=$$(date +%FT%TZ)" $(BUILDFILE)
|
||||
|
||||
## tidy: tidy go.mod
|
||||
.PHONY: tidy
|
||||
|
||||
172
README.md
172
README.md
@@ -16,6 +16,8 @@ It has SRE experience codified into its analyzers and helps to pull out the most
|
||||
|
||||
<a href="https://www.producthunt.com/posts/k8sgpt?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-k8sgpt" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=389489&theme=light" alt="K8sGPT - K8sGPT gives Kubernetes Superpowers to everyone | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<img src="images/demo4.gif" width=650px; />
|
||||
|
||||
# CLI Installation
|
||||
|
||||
|
||||
@@ -32,7 +34,7 @@ brew install k8sgpt
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.9/k8sgpt_386.rpm
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.6/k8sgpt_386.rpm
|
||||
sudo rpm -ivh k8sgpt_386.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -41,7 +43,7 @@ brew install k8sgpt
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.9/k8sgpt_amd64.rpm
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.6/k8sgpt_amd64.rpm
|
||||
sudo rpm -ivh -i k8sgpt_amd64.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -53,7 +55,7 @@ brew install k8sgpt
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.9/k8sgpt_386.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.6/k8sgpt_386.deb
|
||||
sudo dpkg -i k8sgpt_386.deb
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -61,7 +63,7 @@ brew install k8sgpt
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.9/k8sgpt_amd64.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.6/k8sgpt_amd64.deb
|
||||
sudo dpkg -i k8sgpt_amd64.deb
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -74,14 +76,14 @@ brew install k8sgpt
|
||||
**32 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.9/k8sgpt_386.apk
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.6/k8sgpt_386.apk
|
||||
apk add k8sgpt_386.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
**64 bit:**
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.9/k8sgpt_amd64.apk
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.3.6/k8sgpt_amd64.apk
|
||||
apk add k8sgpt_amd64.apk
|
||||
```
|
||||
<!---x-release-please-end-->x
|
||||
@@ -121,13 +123,12 @@ _This mode of operation is ideal for continuous monitoring of your cluster and c
|
||||
|
||||
* Currently the default AI provider is OpenAI, you will need to generate an API key from [OpenAI](https://openai.com)
|
||||
* You can do this by running `k8sgpt generate` to open a browser link to generate it
|
||||
* Run `k8sgpt auth` to set it in k8sgpt.
|
||||
* Run `k8sgpt auth add` to set it in k8sgpt.
|
||||
* You can provide the password directly using the `--password` flag.
|
||||
* Run `k8sgpt filters` to manage the active filters used by the analyzer. By default, all filters are executed during analysis.
|
||||
* Run `k8sgpt analyze` to run a scan.
|
||||
* And use `k8sgpt analyze --explain` to get a more detailed explanation of the issues.
|
||||
|
||||
<img src="images/demo4.gif" width=650px; />
|
||||
* You also run `k8sgpt analyze --with-doc` (with or without the explain flag) to get the official documention from kubernetes.
|
||||
|
||||
## Analyzers
|
||||
|
||||
@@ -161,8 +162,9 @@ _Run a scan with the default analyzers_
|
||||
|
||||
```
|
||||
k8sgpt generate
|
||||
k8sgpt auth
|
||||
k8sgpt auth add
|
||||
k8sgpt analyze --explain
|
||||
k8sgpt analyze --explain --with-doc
|
||||
```
|
||||
|
||||
_Filter on resource_
|
||||
@@ -188,8 +190,8 @@ _Anonymize during explain_
|
||||
k8sgpt analyze --explain --filter=Service --output=json --anonymize
|
||||
```
|
||||
|
||||
### Using filters
|
||||
<details>
|
||||
<summary> Using filters </summary>
|
||||
|
||||
_List filters_
|
||||
|
||||
@@ -208,7 +210,7 @@ k8sgpt filters add [filter(s)]
|
||||
- Simple filter : `k8sgpt filters add Service`
|
||||
- Multiple filters : `k8sgpt filters add Ingress,Pod`
|
||||
|
||||
_Add default filters_
|
||||
_Remove default filters_
|
||||
|
||||
```
|
||||
k8sgpt filters remove [filter(s)]
|
||||
@@ -221,11 +223,20 @@ k8sgpt filters remove [filter(s)]
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
### Additional commands
|
||||
|
||||
<details>
|
||||
|
||||
<summary> Additional commands </summary>
|
||||
_List configured backends_
|
||||
|
||||
```
|
||||
k8sgpt auth list
|
||||
```
|
||||
|
||||
_Remove configured backends_
|
||||
|
||||
```
|
||||
k8sgpt auth remove $MY_BACKEND1,$MY_BACKEND2..
|
||||
```
|
||||
|
||||
_List integrations_
|
||||
|
||||
@@ -264,33 +275,14 @@ curl -X GET "http://localhost:8080/analyze?namespace=k8sgpt&explain=false"
|
||||
```
|
||||
</details>
|
||||
|
||||
## Additional AI providers
|
||||
|
||||
### Azure OpenAI
|
||||
<em>Prerequisites:</em> an Azure OpenAI deployment is needed, please visit MS official [documentation](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource) to create your own.
|
||||
|
||||
To authenticate with k8sgpt, you will need the Azure OpenAI endpoint of your tenant `"https://your Azure OpenAI Endpoint"`, the api key to access your deployment, the deployment name of your model and the model name itself.
|
||||
<details>
|
||||
|
||||
### Run k8sgpt
|
||||
To run k8sgpt, run `k8sgpt auth` with the `azureopenai` backend:
|
||||
```
|
||||
k8sgpt auth --backend azureopenai --baseurl https://<your Azure OpenAI endpoint> --engine <deployment_name> --model <model_name>
|
||||
```
|
||||
Lastly, enter your Azure API key, after the prompt.
|
||||
|
||||
Now you are ready to analyze with the azure openai backend:
|
||||
```
|
||||
k8sgpt analyze --explain --backend azureopenai
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Running local models
|
||||
|
||||
To run local models, it is possible to use OpenAI compatible APIs, for instance [LocalAI](https://github.com/go-skynet/LocalAI) which uses [llama.cpp](https://github.com/ggerganov/llama.cpp) and [ggml](https://github.com/ggerganov/ggml) to run inference on consumer-grade hardware. Models supported by LocalAI for instance are Vicuna, Alpaca, LLaMA, Cerebras, GPT4ALL, GPT4ALL-J and koala.
|
||||
## Key Features
|
||||
|
||||
<details>
|
||||
<summary> LocalAI provider </summary>
|
||||
|
||||
To run local models, it is possible to use OpenAI compatible APIs, for instance [LocalAI](https://github.com/go-skynet/LocalAI) which uses [llama.cpp](https://github.com/ggerganov/llama.cpp) and [ggml](https://github.com/ggerganov/ggml) to run inference on consumer-grade hardware. Models supported by LocalAI for instance are Vicuna, Alpaca, LLaMA, Cerebras, GPT4ALL, GPT4ALL-J and koala.
|
||||
|
||||
|
||||
To run local inference, you need to download the models first, for instance you can find `ggml` compatible models in [huggingface.com](https://huggingface.co/models?search=ggml) (for example vicuna, alpaca and koala).
|
||||
|
||||
@@ -300,10 +292,10 @@ To start the API server, follow the instruction in [LocalAI](https://github.com/
|
||||
|
||||
### Run k8sgpt
|
||||
|
||||
To run k8sgpt, run `k8sgpt auth` with the `localai` backend:
|
||||
To run k8sgpt, run `k8sgpt auth new` with the `localai` backend:
|
||||
|
||||
```
|
||||
k8sgpt auth --backend localai --model <model_name> --baseurl http://localhost:8080/v1
|
||||
k8sgpt auth new --backend localai --model <model_name> --baseurl http://localhost:8080/v1
|
||||
```
|
||||
|
||||
Now you can analyze with the `localai` backend:
|
||||
@@ -312,14 +304,69 @@ Now you can analyze with the `localai` backend:
|
||||
k8sgpt analyze --explain --backend localai
|
||||
```
|
||||
|
||||
</details>
|
||||
</details>
|
||||
|
||||
## How does anonymization work?
|
||||
<details>
|
||||
<summary> AzureOpenAI provider </summary>
|
||||
|
||||
<em>Prerequisites:</em> an Azure OpenAI deployment is needed, please visit MS official [documentation](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource) to create your own.
|
||||
|
||||
To authenticate with k8sgpt, you will need the Azure OpenAI endpoint of your tenant `"https://your Azure OpenAI Endpoint"`, the api key to access your deployment, the deployment name of your model and the model name itself.
|
||||
|
||||
|
||||
To run k8sgpt, run `k8sgpt auth` with the `azureopenai` backend:
|
||||
```
|
||||
k8sgpt auth add --backend azureopenai --baseurl https://<your Azure OpenAI endpoint> --engine <deployment_name> --model <model_name>
|
||||
```
|
||||
Lastly, enter your Azure API key, after the prompt.
|
||||
|
||||
Now you are ready to analyze with the azure openai backend:
|
||||
```
|
||||
k8sgpt analyze --explain --backend azureopenai
|
||||
```
|
||||
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Setting a new default AI provider</summary>
|
||||
|
||||
There may be scenarios where you wish to have K8sGPT plugged into several default AI providers. In this case you may wish to use one as a new default, other than OpenAI which is the project default.
|
||||
|
||||
_To view available providers_
|
||||
|
||||
```
|
||||
k8sgpt auth list
|
||||
Default:
|
||||
> openai
|
||||
Active:
|
||||
> openai
|
||||
> azureopenai
|
||||
Unused:
|
||||
> localai
|
||||
> noopai
|
||||
|
||||
```
|
||||
|
||||
|
||||
_To set a new default provider_
|
||||
|
||||
```
|
||||
k8sgpt auth default -p azureopenai
|
||||
Default provider set to azureopenai
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
With this option, the data is anonymized before being sent to the AI Backend. During the analysis execution, `k8sgpt` retrieves sensitive data (Kubernetes object names, labels, etc.). This data is masked when sent to the AI backend and replaced by a key that can be used to de-anonymize the data when the solution is returned to the user.
|
||||
|
||||
<details>
|
||||
|
||||
With this option, the data is anonymized before being sent to the AI Backend. During the analysis execution, `k8sgpt` retrieves sensitive data (Kubernetes object names, labels, etc.). This data is masked when sent to the AI backend and replaced by a key that can be used to de-anonymize the data when the solution is returned to the user.
|
||||
|
||||
|
||||
<summary> Anonymization </summary>
|
||||
1. Error reported during analysis:
|
||||
```bash
|
||||
Error: HorizontalPodAutoscaler uses StatefulSet/fake-deployment as ScaleTargetRef which does not exist.
|
||||
@@ -344,19 +391,50 @@ The Kubernetes system is trying to scale a StatefulSet named fake-deployment usi
|
||||
|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
|
||||
<details>
|
||||
<summary> Configuration management</summary>
|
||||
`k8sgpt` stores config data in the `$XDG_CONFIG_HOME/k8sgpt/k8sgpt.yaml` file. The data is stored in plain text, including your OpenAI key.
|
||||
|
||||
Config file locations:
|
||||
| OS | Path |
|
||||
|---------|--------------------------------------------------|
|
||||
| ------- | ------------------------------------------------ |
|
||||
| MacOS | ~/Library/Application Support/k8sgpt/k8sgpt.yaml |
|
||||
| Linux | ~/.config/k8sgpt/k8sgpt.yaml |
|
||||
| Windows | %LOCALAPPDATA%/k8sgpt/k8sgpt.yaml |
|
||||
</details>
|
||||
|
||||
<details>
|
||||
There may be scenarios where caching remotely is prefered.
|
||||
In these scenarios K8sGPT supports AWS S3 Integration.
|
||||
|
||||
<summary> Remote caching </summary>
|
||||
|
||||
_As a prerequisite `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are required as environmental variables._
|
||||
|
||||
_Adding a remote cache_
|
||||
Note: this will create the bucket if it does not exist
|
||||
```
|
||||
k8sgpt cache add --region <aws region> --bucket <name>
|
||||
```
|
||||
|
||||
_Listing cache items_
|
||||
```
|
||||
k8sgpt cache list
|
||||
```
|
||||
|
||||
_Removing the remote cache_
|
||||
Note: this will not delete the bucket
|
||||
```
|
||||
k8sgpt cache remove --bucket <name>
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
Find our official documentation available [here](https://docs.k8sgpt.ai)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read our [contributing guide](./CONTRIBUTING.md).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: v0.2.4 #x-release-please-version
|
||||
appVersion: v0.3.0 #x-release-please-version
|
||||
description: A Helm chart for K8SGPT
|
||||
name: k8sgpt
|
||||
type: application
|
||||
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
app.kubernetes.io/name: {{ include "k8sgpt.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
serviceAccountName: k8sgpt
|
||||
serviceAccountName: {{ template "k8sgpt.fullname" . }}
|
||||
containers:
|
||||
- name: k8sgpt-container
|
||||
imagePullPolicy: {{ .Values.deployment.imagePullPolicy }}
|
||||
|
||||
@@ -11,5 +11,5 @@ subjects:
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: k8sgpt-cluster-role-all
|
||||
name: {{ template "k8sgpt.fullname" . }}
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
@@ -16,4 +16,7 @@ spec:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
- name: metrics
|
||||
port: 8081
|
||||
targetPort: 8081
|
||||
type: {{ .Values.service.type }}
|
||||
|
||||
@@ -13,7 +13,7 @@ spec:
|
||||
endpoints:
|
||||
- honorLabels: true
|
||||
path: /metrics
|
||||
port: http
|
||||
port: metrics
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "k8sgpt.name" . }}
|
||||
|
||||
@@ -32,6 +32,7 @@ var (
|
||||
namespace string
|
||||
anonymize bool
|
||||
maxConcurrency int
|
||||
withDoc bool
|
||||
)
|
||||
|
||||
// AnalyzeCmd represents the problems command
|
||||
@@ -45,7 +46,7 @@ var AnalyzeCmd = &cobra.Command{
|
||||
|
||||
// AnalysisResult configuration
|
||||
config, err := analysis.NewAnalysis(backend,
|
||||
language, filters, namespace, nocache, explain, maxConcurrency)
|
||||
language, filters, namespace, nocache, explain, maxConcurrency, withDoc)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
@@ -91,4 +92,6 @@ func init() {
|
||||
AnalyzeCmd.Flags().StringVarP(&language, "language", "l", "english", "Languages to use for AI (e.g. 'English', 'Spanish', 'French', 'German', 'Italian', 'Portuguese', 'Dutch', 'Russian', 'Chinese', 'Japanese', 'Korean')")
|
||||
// add max concurrency
|
||||
AnalyzeCmd.Flags().IntVarP(&maxConcurrency, "max-concurrency", "m", 10, "Maximum number of concurrent requests to the Kubernetes API server")
|
||||
// kubernetes doc flag
|
||||
AnalyzeCmd.Flags().BoolVarP(&withDoc, "with-doc", "d", false, "Give me the official documentation of the involved field")
|
||||
}
|
||||
|
||||
126
cmd/auth/add.go
Normal file
126
cmd/auth/add.go
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Configure new provider",
|
||||
Long: "The new command allows to configure a new backend AI provider",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
backend, _ := cmd.Flags().GetString("backend")
|
||||
if strings.ToLower(backend) == "azureopenai" {
|
||||
_ = cmd.MarkFlagRequired("engine")
|
||||
_ = cmd.MarkFlagRequired("baseurl")
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// get ai configuration
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// search for provider with same name
|
||||
providerIndex := -1
|
||||
for i, provider := range configAI.Providers {
|
||||
if backend == provider.Name {
|
||||
providerIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
validBackend := func(validBackends []string, backend string) bool {
|
||||
for _, b := range validBackends {
|
||||
if b == backend {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// check if backend is not empty and a valid value
|
||||
if backend == "" || !validBackend(ai.Backends, backend) {
|
||||
color.Red("Error: Backend AI cannot be empty and accepted values are '%v'", strings.Join(ai.Backends, ", "))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// check if model is not empty
|
||||
if model == "" {
|
||||
color.Red("Error: Model cannot be empty.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if ai.NeedPassword(backend) && password == "" {
|
||||
fmt.Printf("Enter %s Key: ", backend)
|
||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
color.Red("Error reading %s Key from stdin: %s", backend,
|
||||
err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
password = strings.TrimSpace(string(bytePassword))
|
||||
}
|
||||
|
||||
// create new provider object
|
||||
newProvider := ai.AIProvider{
|
||||
Name: backend,
|
||||
Model: model,
|
||||
Password: password,
|
||||
BaseURL: baseURL,
|
||||
Engine: engine,
|
||||
}
|
||||
|
||||
if providerIndex == -1 {
|
||||
// provider with same name does not exist, add new provider to list
|
||||
configAI.Providers = append(configAI.Providers, newProvider)
|
||||
viper.Set("ai", configAI)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
color.Red("Error writing config file: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
color.Green("%s added to the AI backend provider list", backend)
|
||||
} else {
|
||||
// provider with same name exists, update provider info
|
||||
color.Yellow("Provider with same name already exists.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// add flag for backend
|
||||
addCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
|
||||
// add flag for model
|
||||
addCmd.Flags().StringVarP(&model, "model", "m", "gpt-3.5-turbo", "Backend AI model")
|
||||
// add flag for password
|
||||
addCmd.Flags().StringVarP(&password, "password", "p", "", "Backend AI password")
|
||||
// add flag for url
|
||||
addCmd.Flags().StringVarP(&baseURL, "baseurl", "u", "", "URL AI provider, (e.g `http://localhost:8080/v1`)")
|
||||
// add flag for azure open ai engine/deployment name
|
||||
addCmd.Flags().StringVarP(&engine, "engine", "e", "", "Azure AI deployment name")
|
||||
}
|
||||
113
cmd/auth/auth.go
113
cmd/auth/auth.go
@@ -14,16 +14,8 @@ limitations under the License.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -34,107 +26,28 @@ var (
|
||||
engine string
|
||||
)
|
||||
|
||||
var configAI ai.AIConfiguration
|
||||
|
||||
// authCmd represents the auth command
|
||||
var AuthCmd = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Authenticate with your chosen backend",
|
||||
Long: `Provide the necessary credentials to authenticate with your chosen backend.`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
backend, _ := cmd.Flags().GetString("backend")
|
||||
if strings.ToLower(backend) == "azureopenai" {
|
||||
cmd.MarkFlagRequired("engine")
|
||||
cmd.MarkFlagRequired("baseurl")
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// get ai configuration
|
||||
var configAI ai.AIConfiguration
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
if len(args) == 0 {
|
||||
_ = cmd.Help()
|
||||
return
|
||||
}
|
||||
|
||||
// search for provider with same name
|
||||
providerIndex := -1
|
||||
for i, provider := range configAI.Providers {
|
||||
if backend == provider.Name {
|
||||
providerIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
validBackend := func(validBackends []string, backend string) bool {
|
||||
for _, b := range validBackends {
|
||||
if b == backend {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// check if backend is not empty and a valid value
|
||||
if backend == "" || !validBackend(ai.Backends, backend) {
|
||||
color.Red("Error: Backend AI cannot be empty and accepted values are '%v'", strings.Join(ai.Backends, ", "))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
color.Green("Using %s as backend AI provider", backend)
|
||||
|
||||
// check if model is not empty
|
||||
if model == "" {
|
||||
color.Red("Error: Model cannot be empty.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if ai.NeedPassword(backend) && password == "" {
|
||||
fmt.Printf("Enter %s Key: ", backend)
|
||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
color.Red("Error reading %s Key from stdin: %s", backend,
|
||||
err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
password = strings.TrimSpace(string(bytePassword))
|
||||
}
|
||||
|
||||
// create new provider object
|
||||
newProvider := ai.AIProvider{
|
||||
Name: backend,
|
||||
Model: model,
|
||||
Password: password,
|
||||
BaseURL: baseURL,
|
||||
Engine: engine,
|
||||
}
|
||||
|
||||
if providerIndex == -1 {
|
||||
// provider with same name does not exist, add new provider to list
|
||||
configAI.Providers = append(configAI.Providers, newProvider)
|
||||
color.Green("New provider added")
|
||||
} else {
|
||||
// provider with same name exists, update provider info
|
||||
configAI.Providers[providerIndex] = newProvider
|
||||
color.Green("Provider updated")
|
||||
}
|
||||
viper.Set("ai", configAI)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
color.Red("Error writing config file: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
color.Green("key added")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// add flag for backend
|
||||
AuthCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
|
||||
// add flag for model
|
||||
AuthCmd.Flags().StringVarP(&model, "model", "m", "gpt-3.5-turbo", "Backend AI model")
|
||||
// add flag for password
|
||||
AuthCmd.Flags().StringVarP(&password, "password", "p", "", "Backend AI password")
|
||||
// add flag for url
|
||||
AuthCmd.Flags().StringVarP(&baseURL, "baseurl", "u", "", "URL AI provider, (e.g `http://localhost:8080/v1`)")
|
||||
// add flag for azure open ai engine/deployment name
|
||||
AuthCmd.Flags().StringVarP(&engine, "engine", "e", "", "Azure AI deployment name")
|
||||
// add subcommand to list backends
|
||||
AuthCmd.AddCommand(listCmd)
|
||||
// add subcommand to create new backend provider
|
||||
AuthCmd.AddCommand(addCmd)
|
||||
// add subcommand to remove new backend provider
|
||||
AuthCmd.AddCommand(removeCmd)
|
||||
// add subcommand to set default backend provider
|
||||
AuthCmd.AddCommand(defaultCmd)
|
||||
}
|
||||
|
||||
79
cmd/auth/default.go
Normal file
79
cmd/auth/default.go
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
providerName string
|
||||
)
|
||||
|
||||
var defaultCmd = &cobra.Command{
|
||||
Use: "default",
|
||||
Short: "Set your default AI backend provider",
|
||||
Long: "The command to set your new default AI backend provider (default is openai)",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if providerName == "" {
|
||||
if configAI.DefaultProvider != "" {
|
||||
color.Yellow("Your default provider is %s", configAI.DefaultProvider)
|
||||
} else {
|
||||
color.Yellow("Your default provider is openai")
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
// lowercase the provider name
|
||||
providerName = strings.ToLower(providerName)
|
||||
|
||||
// Check if the provider is in the provider list
|
||||
providerExists := false
|
||||
for _, provider := range configAI.Providers {
|
||||
if provider.Name == providerName {
|
||||
providerExists = true
|
||||
}
|
||||
}
|
||||
if !providerExists {
|
||||
color.Red("Error: Provider %s does not exist", providerName)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Set the default provider
|
||||
configAI.DefaultProvider = providerName
|
||||
|
||||
viper.Set("ai", configAI)
|
||||
// Viper write config
|
||||
err = viper.WriteConfig()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Print acknowledgement
|
||||
color.Green("Default provider set to %s", providerName)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// provider name flag
|
||||
defaultCmd.Flags().StringVarP(&providerName, "provider", "p", "", "The name of the provider to set as default")
|
||||
}
|
||||
73
cmd/auth/list.go
Normal file
73
cmd/auth/list.go
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured providers",
|
||||
Long: "The list command displays a list of configured providers",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// get ai configuration
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Print the default if it is set
|
||||
fmt.Print(color.YellowString("Default: \n"))
|
||||
if configAI.DefaultProvider != "" {
|
||||
fmt.Printf("> %s\n", color.BlueString(configAI.DefaultProvider))
|
||||
} else {
|
||||
fmt.Printf("> %s\n", color.BlueString("openai"))
|
||||
}
|
||||
|
||||
// Get list of all AI Backends and only print htem if they are not in the provider list
|
||||
fmt.Print(color.YellowString("Active: \n"))
|
||||
for _, aiBackend := range ai.Backends {
|
||||
providerExists := false
|
||||
for _, provider := range configAI.Providers {
|
||||
if provider.Name == aiBackend {
|
||||
providerExists = true
|
||||
}
|
||||
}
|
||||
if providerExists {
|
||||
fmt.Printf("> %s\n", color.GreenString(aiBackend))
|
||||
}
|
||||
}
|
||||
fmt.Print(color.YellowString("Unused: \n"))
|
||||
for _, aiBackend := range ai.Backends {
|
||||
providerExists := false
|
||||
for _, provider := range configAI.Providers {
|
||||
if provider.Name == aiBackend {
|
||||
providerExists = true
|
||||
}
|
||||
}
|
||||
if !providerExists {
|
||||
fmt.Printf("> %s\n", color.RedString(aiBackend))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
71
cmd/auth/remove.go
Normal file
71
cmd/auth/remove.go
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var removeCmd = &cobra.Command{
|
||||
Use: "remove [backend(s)]",
|
||||
Short: "Remove a provider",
|
||||
Long: "The command to remove an AI backend provider",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
inputBackends := strings.Split(args[0], ",")
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(inputBackends) == 0 {
|
||||
color.Red("Error: backend must be set.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, b := range inputBackends {
|
||||
foundBackend := false
|
||||
for i, provider := range configAI.Providers {
|
||||
if b == provider.Name {
|
||||
foundBackend = true
|
||||
configAI.Providers = append(configAI.Providers[:i], configAI.Providers[i+1:]...)
|
||||
color.Green("%s deleted to the AI backend provider list", b)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBackend {
|
||||
color.Red("Error: %s does not exist in configuration file. Please use k8sgpt auth new.", backend)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
}
|
||||
viper.Set("ai", configAI)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
color.Red("Error writing config file: %s", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
}
|
||||
|
||||
54
cmd/cache/add.go
vendored
Normal file
54
cmd/cache/add.go
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
region string
|
||||
)
|
||||
|
||||
// addCmd represents the add command
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add a remote cache",
|
||||
Long: `This command allows you to add a remote cache to store the results of an analysis.
|
||||
The supported cache types are:
|
||||
- S3`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(color.YellowString("Adding remote S3 based cache"))
|
||||
err := cache.AddRemoteCache(bucketname, region)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
CacheCmd.AddCommand(addCmd)
|
||||
addCmd.Flags().StringVarP(®ion, "region", "r", "", "The region to use for the cache")
|
||||
addCmd.Flags().StringVarP(&bucketname, "bucket", "b", "", "The name of the bucket to use for the cache")
|
||||
addCmd.MarkFlagRequired("bucket")
|
||||
addCmd.MarkFlagRequired("region")
|
||||
|
||||
}
|
||||
36
cmd/cache/cache.go
vendored
Normal file
36
cmd/cache/cache.go
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
bucketname string
|
||||
)
|
||||
|
||||
// cacheCmd represents the cache command
|
||||
var CacheCmd = &cobra.Command{
|
||||
Use: "cache",
|
||||
Short: "For working with the cache the results of an analysis",
|
||||
Long: `Cache commands allow you to add a remote cache, list the contents of the cache, and remove items from the cache.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
}
|
||||
55
cmd/cache/list.go
vendored
Normal file
55
cmd/cache/list.go
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// listCmd represents the list command
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List the contents of the cache",
|
||||
Long: `This command allows you to list the contents of the cache.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// load remote cache if it is configured
|
||||
remoteCacheEnabled, err := cache.RemoteCacheEnabled()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
c := cache.New(false, remoteCacheEnabled)
|
||||
// list the contents of the cache
|
||||
names, err := c.List()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
println(name)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
CacheCmd.AddCommand(listCmd)
|
||||
|
||||
}
|
||||
43
cmd/cache/remove.go
vendored
Normal file
43
cmd/cache/remove.go
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cache
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// removeCmd represents the remove command
|
||||
var removeCmd = &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "Remove the remote cache",
|
||||
Long: `This command allows you to remove the remote cache and use the default filecache.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
err := cache.RemoveRemoteCache(bucketname)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
color.Green("Successfully removed the remote cache")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
CacheCmd.AddCommand(removeCmd)
|
||||
}
|
||||
@@ -25,7 +25,7 @@ var FiltersCmd = &cobra.Command{
|
||||
You can list available filters to analyze resources.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
cmd.Help()
|
||||
_ = cmd.Help()
|
||||
return
|
||||
}
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ var listCmd = &cobra.Command{
|
||||
activeFilters = coreFilters
|
||||
}
|
||||
inactiveFilters := util.SliceDiff(availableFilters, activeFilters)
|
||||
fmt.Printf(color.YellowString("Active: \n"))
|
||||
fmt.Print(color.YellowString("Active: \n"))
|
||||
for _, filter := range activeFilters {
|
||||
|
||||
// if the filter is an integration, mark this differently
|
||||
@@ -50,7 +50,7 @@ var listCmd = &cobra.Command{
|
||||
|
||||
// display inactive filters
|
||||
if len(inactiveFilters) != 0 {
|
||||
fmt.Printf(color.YellowString("Unused: \n"))
|
||||
fmt.Print(color.YellowString("Unused: \n"))
|
||||
for _, filter := range inactiveFilters {
|
||||
// if the filter is an integration, mark this differently
|
||||
if util.SliceContainsString(integrationFilters, filter) {
|
||||
|
||||
@@ -15,8 +15,10 @@ package integration
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/integration"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// activateCmd represents the activate command
|
||||
@@ -27,10 +29,17 @@ var activateCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
integrationName := args[0]
|
||||
coreFilters, _, _ := analyzer.ListFilters()
|
||||
|
||||
// Update filters
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
if len(activeFilters) == 0 {
|
||||
activeFilters = coreFilters
|
||||
}
|
||||
|
||||
integration := integration.NewIntegration()
|
||||
// Check if the integation exists
|
||||
err := integration.Activate(integrationName, namespace)
|
||||
err := integration.Activate(integrationName, namespace, activeFilters)
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
return
|
||||
|
||||
@@ -32,7 +32,7 @@ var IntegrationCmd = &cobra.Command{
|
||||
|
||||
This would allow you to deploy trivy into your cluster and use a K8sGPT analyzer to parse trivy results.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
_ = cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
15
cmd/root.go
15
cmd/root.go
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/analyze"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/auth"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/filters"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/generate"
|
||||
"github.com/k8sgpt-ai/k8sgpt/cmd/integration"
|
||||
@@ -33,7 +34,9 @@ var (
|
||||
cfgFile string
|
||||
kubecontext string
|
||||
kubeconfig string
|
||||
version string
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
@@ -48,8 +51,10 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute(v string) {
|
||||
version = v
|
||||
func Execute(v string, c string, d string) {
|
||||
Version = v
|
||||
Commit = c
|
||||
Date = d
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
@@ -67,6 +72,7 @@ func init() {
|
||||
rootCmd.AddCommand(generate.GenerateCmd)
|
||||
rootCmd.AddCommand(integration.IntegrationCmd)
|
||||
rootCmd.AddCommand(serve.ServeCmd)
|
||||
rootCmd.AddCommand(cache.CacheCmd)
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.k8sgpt.yaml)")
|
||||
rootCmd.PersistentFlags().StringVar(&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.")
|
||||
@@ -85,7 +91,7 @@ func initConfig() {
|
||||
viper.SetConfigType("yaml")
|
||||
viper.SetConfigName("k8sgpt")
|
||||
|
||||
viper.SafeWriteConfig()
|
||||
_ = viper.SafeWriteConfig()
|
||||
}
|
||||
|
||||
viper.Set("kubecontext", kubecontext)
|
||||
@@ -96,6 +102,7 @@ func initConfig() {
|
||||
|
||||
// If a config file is found, read it in.
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
_ = 1
|
||||
// fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,13 @@ import (
|
||||
k8sgptserver "github.com/k8sgpt-ai/k8sgpt/pkg/server"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
port string
|
||||
backend string
|
||||
port string
|
||||
metricsPort string
|
||||
backend string
|
||||
)
|
||||
|
||||
var ServeCmd = &cobra.Command{
|
||||
@@ -90,23 +92,42 @@ var ServeCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
server := k8sgptserver.Config{
|
||||
Backend: aiProvider.Name,
|
||||
Port: port,
|
||||
Token: aiProvider.Password,
|
||||
}
|
||||
|
||||
err = server.Serve()
|
||||
logger, err := zap.NewProduction()
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
color.Red("failed to create logger: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// override the default backend if a flag is provided
|
||||
defer logger.Sync()
|
||||
|
||||
server := k8sgptserver.Config{
|
||||
Backend: aiProvider.Name,
|
||||
Port: port,
|
||||
MetricsPort: metricsPort,
|
||||
Token: aiProvider.Password,
|
||||
Logger: logger,
|
||||
}
|
||||
go func() {
|
||||
if err := server.ServeMetrics(); err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := server.Serve(); err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for both servers to exit
|
||||
select {}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// add flag for backend
|
||||
ServeCmd.Flags().StringVarP(&port, "port", "p", "8080", "Port to run the server on")
|
||||
ServeCmd.Flags().StringVarP(&metricsPort, "metrics-port", "", "8081", "Port to run the metrics-server on")
|
||||
ServeCmd.Flags().StringVarP(&backend, "backend", "b", "openai", "Backend AI provider")
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ limitations under the License.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -23,7 +26,21 @@ var versionCmd = &cobra.Command{
|
||||
Short: "Print the version number of k8sgpt",
|
||||
Long: `All software has versions. This is k8sgpt's`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Printf("k8sgpt version %s\n", version)
|
||||
if Version == "dev" {
|
||||
details, ok := debug.ReadBuildInfo()
|
||||
if ok && details.Main.Version != "" && details.Main.Version != "(devel)" {
|
||||
Version = details.Main.Version
|
||||
for _, i := range details.Settings {
|
||||
if i.Key == "vcs.time" {
|
||||
Date = i.Value
|
||||
}
|
||||
if i.Key == "vcs.revision" {
|
||||
Commit = i.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("k8sgpt: %s (%s), built at: %s\n", Version, Commit, Date)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
FROM golang:1.20.4-alpine3.16 AS builder
|
||||
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
ARG VERSION
|
||||
ARG COMMIT
|
||||
ARG DATE
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
@@ -20,7 +22,7 @@ RUN go mod download
|
||||
|
||||
COPY ./ ./
|
||||
|
||||
RUN go build -o /workspace/k8sgpt ./
|
||||
RUN go build -o /workspace/k8sgpt -ldflags "-X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" ./
|
||||
|
||||
FROM gcr.io/distroless/static AS production
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"showLineNumbers": false,
|
||||
"showMiniMap": false
|
||||
},
|
||||
"content": "",
|
||||
"content": "",
|
||||
"mode": "markdown"
|
||||
},
|
||||
"pluginVersion": "9.4.7",
|
||||
|
||||
60
go.mod
60
go.mod
@@ -7,22 +7,30 @@ require (
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/magiconair/properties v1.8.7
|
||||
github.com/mittwald/go-helm-client v0.12.1
|
||||
github.com/sashabaranov/go-openai v1.9.2
|
||||
github.com/sashabaranov/go-openai v1.9.3
|
||||
github.com/schollz/progressbar/v3 v3.13.1
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
golang.org/x/term v0.7.0
|
||||
helm.sh/helm/v3 v3.11.3
|
||||
k8s.io/api v0.26.3
|
||||
k8s.io/apimachinery v0.26.3
|
||||
k8s.io/client-go v0.26.3
|
||||
k8s.io/kubectl v0.26.3
|
||||
github.com/spf13/viper v1.16.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/term v0.8.0
|
||||
helm.sh/helm/v3 v3.12.0
|
||||
k8s.io/api v0.27.2
|
||||
k8s.io/apimachinery v0.27.2
|
||||
k8s.io/client-go v0.27.2
|
||||
k8s.io/kubectl v0.27.2
|
||||
|
||||
)
|
||||
|
||||
require github.com/adrg/xdg v0.4.0
|
||||
|
||||
require (
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20230524215339-41d88e13ab7e.1
|
||||
buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.30.0-20230524215339-41d88e13ab7e.1
|
||||
github.com/aws/aws-sdk-go v1.44.272
|
||||
)
|
||||
|
||||
require github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
|
||||
require (
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
@@ -46,7 +54,7 @@ require (
|
||||
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/cli v23.0.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/docker v23.0.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
@@ -67,7 +75,7 @@ require (
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/gnostic v0.6.9
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/go-containerregistry v0.14.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
@@ -111,7 +119,7 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221020182949-4df8887994e8 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
@@ -128,7 +136,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spdx/tools-golang v0.5.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
@@ -142,34 +150,34 @@ require (
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230124195608-d38c7dcee874 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/oauth2 v0.6.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/oauth2 v0.7.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||
google.golang.org/grpc v1.54.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/grpc v1.55.0
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.26.3 // indirect
|
||||
k8s.io/apiserver v0.26.3 // indirect
|
||||
k8s.io/cli-runtime v0.26.3 // indirect
|
||||
k8s.io/component-base v0.26.3 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.27.1 // indirect
|
||||
k8s.io/apiserver v0.27.1 // indirect
|
||||
k8s.io/cli-runtime v0.27.2 // indirect
|
||||
k8s.io/component-base v0.27.2 // indirect
|
||||
k8s.io/klog/v2 v2.90.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230327201221-f5883ff37f0c // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
|
||||
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 // indirect
|
||||
oras.land/oras-go v1.2.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/kustomize/api v0.12.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.13.2 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.14.1 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
8
main.go
8
main.go
@@ -15,8 +15,12 @@ package main
|
||||
|
||||
import "github.com/k8sgpt-ai/k8sgpt/cmd"
|
||||
|
||||
var version = "dev"
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "HEAD"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute(version)
|
||||
cmd.Execute(version, commit, date)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ func NewClient(provider string) IAI {
|
||||
}
|
||||
|
||||
type AIConfiguration struct {
|
||||
Providers []AIProvider `mapstructure:"providers"`
|
||||
Providers []AIProvider `mapstructure:"providers"`
|
||||
DefaultProvider string `mapstructure:"defaultprovider"`
|
||||
}
|
||||
|
||||
type AIProvider struct {
|
||||
|
||||
@@ -57,7 +57,7 @@ func (a *NoOpAIClient) Parse(ctx context.Context, prompt []string, cache cache.I
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
|
||||
err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
|
||||
|
||||
if err != nil {
|
||||
color.Red("error storing value to cache: %v", err)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package ai
|
||||
|
||||
const (
|
||||
default_prompt = "Simplify the following Kubernetes error message and provide a solution in %s: %s"
|
||||
default_prompt = `Simplify the following Kubernetes error message delimited by triple dashes written in --- %s --- language; --- %s ---.
|
||||
Provide the most possible solution in a step by step style in no more than 280 characters. Write the output in the following format:
|
||||
Error: {Explain error here}
|
||||
Solution: {Step by step solution here}
|
||||
`
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/fatih/color"
|
||||
openapi_v2 "github.com/google/gnostic/openapiv2"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
@@ -34,16 +35,18 @@ import (
|
||||
)
|
||||
|
||||
type Analysis struct {
|
||||
Context context.Context
|
||||
Filters []string
|
||||
Client *kubernetes.Client
|
||||
AIClient ai.IAI
|
||||
Results []common.Result
|
||||
Errors []string
|
||||
Namespace string
|
||||
Cache cache.ICache
|
||||
Explain bool
|
||||
MaxConcurrency int
|
||||
Context context.Context
|
||||
Filters []string
|
||||
Client *kubernetes.Client
|
||||
AIClient ai.IAI
|
||||
Results []common.Result
|
||||
Errors []string
|
||||
Namespace string
|
||||
Cache cache.ICache
|
||||
Explain bool
|
||||
MaxConcurrency int
|
||||
AnalysisAIProvider string // The name of the AI Provider used for this analysis
|
||||
WithDoc bool
|
||||
}
|
||||
|
||||
type AnalysisStatus string
|
||||
@@ -55,13 +58,14 @@ const (
|
||||
)
|
||||
|
||||
type JsonOutput struct {
|
||||
Provider string `json:"provider"`
|
||||
Errors AnalysisErrors `json:"errors"`
|
||||
Status AnalysisStatus `json:"status"`
|
||||
Problems int `json:"problems"`
|
||||
Results []common.Result `json:"results"`
|
||||
}
|
||||
|
||||
func NewAnalysis(backend string, language string, filters []string, namespace string, noCache bool, explain bool, maxConcurrency int) (*Analysis, error) {
|
||||
func NewAnalysis(backend string, language string, filters []string, namespace string, noCache bool, explain bool, maxConcurrency int, withDoc bool) (*Analysis, error) {
|
||||
var configAI ai.AIConfiguration
|
||||
err := viper.UnmarshalKey("ai", &configAI)
|
||||
if err != nil {
|
||||
@@ -74,6 +78,12 @@ func NewAnalysis(backend string, language string, filters []string, namespace st
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Backend string will have high priority than a default provider
|
||||
// Backend as "openai" represents the default CLI argument passed through
|
||||
if configAI.DefaultProvider != "" && backend == "openai" {
|
||||
backend = configAI.DefaultProvider
|
||||
}
|
||||
|
||||
var aiProvider ai.AIProvider
|
||||
for _, provider := range configAI.Providers {
|
||||
if backend == provider.Name {
|
||||
@@ -104,36 +114,56 @@ func NewAnalysis(backend string, language string, filters []string, namespace st
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// load remote cache if it is configured
|
||||
remoteCacheEnabled, err := cache.RemoteCacheEnabled()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Analysis{
|
||||
Context: ctx,
|
||||
Filters: filters,
|
||||
Client: client,
|
||||
AIClient: aiClient,
|
||||
Namespace: namespace,
|
||||
Cache: cache.New(noCache),
|
||||
Explain: explain,
|
||||
MaxConcurrency: maxConcurrency,
|
||||
Context: ctx,
|
||||
Filters: filters,
|
||||
Client: client,
|
||||
AIClient: aiClient,
|
||||
Namespace: namespace,
|
||||
Cache: cache.New(noCache, remoteCacheEnabled),
|
||||
Explain: explain,
|
||||
MaxConcurrency: maxConcurrency,
|
||||
AnalysisAIProvider: backend,
|
||||
WithDoc: withDoc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Analysis) RunAnalysis() {
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
analyzerMap := analyzer.GetAnalyzerMap()
|
||||
coreAnalyzerMap, analyzerMap := analyzer.GetAnalyzerMap()
|
||||
|
||||
// we get the openapi schema from the server only if required by the flag "with-doc"
|
||||
openapiSchema := &openapi_v2.Document{}
|
||||
if a.WithDoc {
|
||||
var openApiErr error
|
||||
|
||||
openapiSchema, openApiErr = a.Client.Client.Discovery().OpenAPISchema()
|
||||
if openApiErr != nil {
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[KubernetesDoc] %s", openApiErr))
|
||||
}
|
||||
}
|
||||
|
||||
analyzerConfig := common.Analyzer{
|
||||
Client: a.Client,
|
||||
Context: a.Context,
|
||||
Namespace: a.Namespace,
|
||||
AIClient: a.AIClient,
|
||||
Client: a.Client,
|
||||
Context: a.Context,
|
||||
Namespace: a.Namespace,
|
||||
AIClient: a.AIClient,
|
||||
OpenapiSchema: openapiSchema,
|
||||
}
|
||||
|
||||
semaphore := make(chan struct{}, a.MaxConcurrency)
|
||||
// if there are no filters selected and no active_filters then run all of them
|
||||
// if there are no filters selected and no active_filters then run coreAnalyzer
|
||||
if len(a.Filters) == 0 && len(activeFilters) == 0 {
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
for _, analyzer := range analyzerMap {
|
||||
for _, analyzer := range coreAnalyzerMap {
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
go func(analyzer common.IAnalyzer, wg *sync.WaitGroup, semaphore chan struct{}) {
|
||||
@@ -141,7 +171,7 @@ func (a *Analysis) RunAnalysis() {
|
||||
results, err := analyzer.Analyze(analyzerConfig)
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
a.Errors = append(a.Errors, fmt.Sprintf(fmt.Sprintf("[%s] %s", reflect.TypeOf(analyzer).Name(), err)))
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", reflect.TypeOf(analyzer).Name(), err))
|
||||
mutex.Unlock()
|
||||
}
|
||||
mutex.Lock()
|
||||
@@ -152,6 +182,7 @@ func (a *Analysis) RunAnalysis() {
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
semaphore = make(chan struct{}, a.MaxConcurrency)
|
||||
// if the filters flag is specified
|
||||
@@ -167,7 +198,7 @@ func (a *Analysis) RunAnalysis() {
|
||||
results, err := analyzer.Analyze(analyzerConfig)
|
||||
if err != nil {
|
||||
mutex.Lock()
|
||||
a.Errors = append(a.Errors, fmt.Sprintf(fmt.Sprintf("[%s] %s", filter, err)))
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
|
||||
mutex.Unlock()
|
||||
}
|
||||
mutex.Lock()
|
||||
@@ -176,10 +207,11 @@ func (a *Analysis) RunAnalysis() {
|
||||
<-semaphore
|
||||
}(analyzer, filter)
|
||||
} else {
|
||||
a.Errors = append(a.Errors, fmt.Sprintf(fmt.Sprintf("\"%s\" filter does not exist. Please run k8sgpt filters list.", filter)))
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("\"%s\" filter does not exist. Please run k8sgpt filters list.", filter))
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -234,7 +266,7 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
// FIXME: can we avoid checking if output is json multiple times?
|
||||
// maybe implement the progress bar better?
|
||||
if output != "json" {
|
||||
bar.Exit()
|
||||
_ = bar.Exit()
|
||||
}
|
||||
|
||||
// Check for exhaustion
|
||||
@@ -255,7 +287,7 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
|
||||
analysis.Details = parsedText
|
||||
if output != "json" {
|
||||
bar.Add(1)
|
||||
_ = bar.Add(1)
|
||||
}
|
||||
a.Results[index] = analysis
|
||||
}
|
||||
|
||||
@@ -14,14 +14,127 @@ limitations under the License.
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/spf13/viper"
|
||||
"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"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// sub-function
|
||||
func analysis_RunAnalysisFilterTester(t *testing.T, filterFlag string) []common.Result {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodPending,
|
||||
Conditions: []v1.PodCondition{
|
||||
{
|
||||
Type: v1.PodScheduled,
|
||||
Reason: "Unschedulable",
|
||||
Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "example",
|
||||
},
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "example",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
analysis := Analysis{
|
||||
Context: context.Background(),
|
||||
Results: []common.Result{},
|
||||
Namespace: "default",
|
||||
MaxConcurrency: 1,
|
||||
Client: &kubernetes.Client{
|
||||
Client: clientset,
|
||||
},
|
||||
}
|
||||
if len(filterFlag) > 0 {
|
||||
// `--filter` is explicitly given
|
||||
analysis.Filters = strings.Split(filterFlag, ",")
|
||||
}
|
||||
analysis.RunAnalysis()
|
||||
return analysis.Results
|
||||
|
||||
}
|
||||
|
||||
// Test: Filter logic with running different Analyzers
|
||||
func TestAnalysis_RunAnalysisWithFilter(t *testing.T) {
|
||||
var results []common.Result
|
||||
var filterFlag string
|
||||
|
||||
//1. Neither --filter flag Nor active filter is specified, only the "core analyzers"
|
||||
results = analysis_RunAnalysisFilterTester(t, "")
|
||||
assert.Equal(t, len(results), 3) // all built-in resource will be analyzed
|
||||
|
||||
//2. When the --filter flag is specified
|
||||
|
||||
filterFlag = "Pod" // --filter=Pod
|
||||
results = analysis_RunAnalysisFilterTester(t, filterFlag)
|
||||
assert.Equal(t, len(results), 1)
|
||||
assert.Equal(t, results[0].Kind, filterFlag)
|
||||
|
||||
filterFlag = "Ingress,Pod" // --filter=Ingress,Pod
|
||||
results = analysis_RunAnalysisFilterTester(t, filterFlag)
|
||||
assert.Equal(t, len(results), 2)
|
||||
}
|
||||
|
||||
// Test: Filter logic with Active Filter
|
||||
func TestAnalysis_RunAnalysisActiveFilter(t *testing.T) {
|
||||
|
||||
//When the --filter flag is not specified but has actived filter in config
|
||||
var results []common.Result
|
||||
|
||||
viper.SetDefault("active_filters", "Ingress")
|
||||
results = analysis_RunAnalysisFilterTester(t, "")
|
||||
assert.Equal(t, len(results), 1)
|
||||
|
||||
viper.SetDefault("active_filters", []string{"Ingress", "Service"})
|
||||
results = analysis_RunAnalysisFilterTester(t, "")
|
||||
assert.Equal(t, len(results), 2)
|
||||
|
||||
viper.SetDefault("active_filters", []string{"Ingress", "Service", "Pod"})
|
||||
results = analysis_RunAnalysisFilterTester(t, "")
|
||||
assert.Equal(t, len(results), 3)
|
||||
}
|
||||
|
||||
func TestAnalysis_NoProblemJsonOutput(t *testing.T) {
|
||||
|
||||
analysis := Analysis{
|
||||
@@ -50,6 +163,7 @@ func TestAnalysis_NoProblemJsonOutput(t *testing.T) {
|
||||
fmt.Println(expected)
|
||||
|
||||
require.Equal(t, got, expected)
|
||||
|
||||
}
|
||||
|
||||
func TestAnalysis_ProblemJsonOutput(t *testing.T) {
|
||||
|
||||
@@ -42,6 +42,7 @@ func (a *Analysis) jsonOutput() ([]byte, error) {
|
||||
}
|
||||
|
||||
result := JsonOutput{
|
||||
Provider: a.AnalysisAIProvider,
|
||||
Problems: problems,
|
||||
Results: a.Results,
|
||||
Errors: a.Errors,
|
||||
@@ -56,6 +57,10 @@ func (a *Analysis) jsonOutput() ([]byte, error) {
|
||||
|
||||
func (a *Analysis) textOutput() ([]byte, error) {
|
||||
var output strings.Builder
|
||||
|
||||
// Print the AI provider used for this analysis
|
||||
output.WriteString(fmt.Sprintf("AI Provider: %s\n", color.YellowString(a.AnalysisAIProvider)))
|
||||
|
||||
if len(a.Errors) != 0 {
|
||||
output.WriteString("\n")
|
||||
output.WriteString(color.YellowString("Warnings : \n"))
|
||||
@@ -73,6 +78,9 @@ func (a *Analysis) textOutput() ([]byte, error) {
|
||||
color.YellowString(result.Name), color.CyanString(result.ParentObject)))
|
||||
for _, err := range result.Error {
|
||||
output.WriteString(fmt.Sprintf("- %s %s\n", color.RedString("Error:"), color.RedString(err.Text)))
|
||||
if err.KubernetesDoc != "" {
|
||||
output.WriteString(fmt.Sprintf(" %s %s\n", color.RedString("Kubernetes Doc:"), color.RedString(err.KubernetesDoc)))
|
||||
}
|
||||
}
|
||||
output.WriteString(color.GreenString(result.Details + "\n"))
|
||||
}
|
||||
|
||||
@@ -78,18 +78,20 @@ func ListFilters() ([]string, []string, []string) {
|
||||
return coreKeys, additionalKeys, integrationAnalyzers
|
||||
}
|
||||
|
||||
func GetAnalyzerMap() map[string]common.IAnalyzer {
|
||||
func GetAnalyzerMap() (map[string]common.IAnalyzer, map[string]common.IAnalyzer) {
|
||||
|
||||
mergedMap := make(map[string]common.IAnalyzer)
|
||||
coreAnalyzer := make(map[string]common.IAnalyzer)
|
||||
mergedAnalyzerMap := make(map[string]common.IAnalyzer)
|
||||
|
||||
// add core analyzer
|
||||
for key, value := range coreAnalyzerMap {
|
||||
mergedMap[key] = value
|
||||
coreAnalyzer[key] = value
|
||||
mergedAnalyzerMap[key] = value
|
||||
}
|
||||
|
||||
// add additional analyzer
|
||||
for key, value := range additionalAnalyzerMap {
|
||||
mergedMap[key] = value
|
||||
mergedAnalyzerMap[key] = value
|
||||
}
|
||||
|
||||
integrationProvider := integration.NewIntegration()
|
||||
@@ -106,9 +108,9 @@ func GetAnalyzerMap() map[string]common.IAnalyzer {
|
||||
fmt.Println(color.RedString(err.Error()))
|
||||
os.Exit(1)
|
||||
}
|
||||
in.AddAnalyzer(&mergedMap)
|
||||
in.AddAnalyzer(&mergedAnalyzerMap)
|
||||
}
|
||||
}
|
||||
|
||||
return mergedMap
|
||||
return coreAnalyzer, mergedAnalyzerMap
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
cron "github.com/robfig/cron/v3"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type CronJobAnalyzer struct{}
|
||||
@@ -28,6 +30,14 @@ type CronJobAnalyzer struct{}
|
||||
func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "CronJob"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "batch",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
@@ -43,8 +53,11 @@ func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, err
|
||||
for _, cronJob := range cronJobList.Items {
|
||||
var failures []common.Failure
|
||||
if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend {
|
||||
doc := apiDoc.GetApiDocV2("spec.suspend")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("CronJob %s is suspended", cronJob.Name),
|
||||
Text: fmt.Sprintf("CronJob %s is suspended", cronJob.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: cronJob.Namespace,
|
||||
@@ -59,8 +72,11 @@ func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, err
|
||||
} else {
|
||||
// check the schedule format
|
||||
if _, err := CheckCronScheduleIsValid(cronJob.Spec.Schedule); err != nil {
|
||||
doc := apiDoc.GetApiDocV2("spec.schedule")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("CronJob %s has an invalid schedule: %s", cronJob.Name, err.Error()),
|
||||
Text: fmt.Sprintf("CronJob %s has an invalid schedule: %s", cronJob.Name, err.Error()),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: cronJob.Namespace,
|
||||
@@ -78,9 +94,11 @@ func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, err
|
||||
if cronJob.Spec.StartingDeadlineSeconds != nil {
|
||||
deadline := time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second
|
||||
if deadline < 0 {
|
||||
doc := apiDoc.GetApiDocV2("spec.startingDeadlineSeconds")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("CronJob %s has a negative starting deadline", cronJob.Name),
|
||||
Text: fmt.Sprintf("CronJob %s has a negative starting deadline", cronJob.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: cronJob.Namespace,
|
||||
|
||||
@@ -18,8 +18,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
)
|
||||
|
||||
@@ -31,6 +33,14 @@ type DeploymentAnalyzer struct {
|
||||
func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "Deployment"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
@@ -45,8 +55,11 @@ 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")
|
||||
|
||||
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),
|
||||
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,
|
||||
|
||||
@@ -17,10 +17,12 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type HpaAnalyzer struct{}
|
||||
@@ -28,6 +30,14 @@ type HpaAnalyzer struct{}
|
||||
func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "HorizontalPodAutoscaler"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "autoscaling",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
@@ -76,8 +86,11 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
}
|
||||
|
||||
if podInfo == nil {
|
||||
doc := apiDoc.GetApiDocV2("spec.scaleTargetRef")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("HorizontalPodAutoscaler uses %s/%s as ScaleTargetRef which does not exist.", scaleTargetRef.Kind, scaleTargetRef.Name),
|
||||
Text: fmt.Sprintf("HorizontalPodAutoscaler uses %s/%s as ScaleTargetRef which does not exist.", scaleTargetRef.Kind, scaleTargetRef.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: scaleTargetRef.Name,
|
||||
@@ -94,8 +107,11 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
}
|
||||
|
||||
if containers <= 0 {
|
||||
doc := apiDoc.GetApiDocV2("spec.scaleTargetRef.kind")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("%s %s/%s does not have resource configured.", scaleTargetRef.Kind, a.Namespace, scaleTargetRef.Name),
|
||||
Text: fmt.Sprintf("%s %s/%s does not have resource configured.", scaleTargetRef.Kind, a.Namespace, scaleTargetRef.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: scaleTargetRef.Name,
|
||||
|
||||
@@ -205,15 +205,15 @@ func TestHPAAnalyzerWithExistingScaleTargetRefAsDeployment(t *testing.T) {
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "example",
|
||||
Name: "example",
|
||||
Image: "nginx",
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
"cpu": resource.MustParse("100m"),
|
||||
"cpu": resource.MustParse("100m"),
|
||||
"memory": resource.MustParse("128Mi"),
|
||||
},
|
||||
Limits: corev1.ResourceList{
|
||||
"cpu": resource.MustParse("200m"),
|
||||
"cpu": resource.MustParse("200m"),
|
||||
"memory": resource.MustParse("256Mi"),
|
||||
},
|
||||
},
|
||||
@@ -269,15 +269,15 @@ func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicationController(t *testing
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "example",
|
||||
Name: "example",
|
||||
Image: "nginx",
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
"cpu": resource.MustParse("100m"),
|
||||
"cpu": resource.MustParse("100m"),
|
||||
"memory": resource.MustParse("128Mi"),
|
||||
},
|
||||
Limits: corev1.ResourceList{
|
||||
"cpu": resource.MustParse("200m"),
|
||||
"cpu": resource.MustParse("200m"),
|
||||
"memory": resource.MustParse("256Mi"),
|
||||
},
|
||||
},
|
||||
@@ -333,15 +333,15 @@ func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicaSet(t *testing.T) {
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "example",
|
||||
Name: "example",
|
||||
Image: "nginx",
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
"cpu": resource.MustParse("100m"),
|
||||
"cpu": resource.MustParse("100m"),
|
||||
"memory": resource.MustParse("128Mi"),
|
||||
},
|
||||
Limits: corev1.ResourceList{
|
||||
"cpu": resource.MustParse("200m"),
|
||||
"cpu": resource.MustParse("200m"),
|
||||
"memory": resource.MustParse("256Mi"),
|
||||
},
|
||||
},
|
||||
@@ -397,15 +397,15 @@ func TestHPAAnalyzerWithExistingScaleTargetRefAsStatefulSet(t *testing.T) {
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "example",
|
||||
Name: "example",
|
||||
Image: "nginx",
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
"cpu": resource.MustParse("100m"),
|
||||
"cpu": resource.MustParse("100m"),
|
||||
"memory": resource.MustParse("128Mi"),
|
||||
},
|
||||
Limits: corev1.ResourceList{
|
||||
"cpu": resource.MustParse("200m"),
|
||||
"cpu": resource.MustParse("200m"),
|
||||
"memory": resource.MustParse("256Mi"),
|
||||
},
|
||||
},
|
||||
@@ -461,7 +461,7 @@ func TestHPAAnalyzerWithExistingScaleTargetRefWithoutSpecifyingResources(t *test
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "example",
|
||||
Name: "example",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
@@ -487,7 +487,7 @@ func TestHPAAnalyzerWithExistingScaleTargetRefWithoutSpecifyingResources(t *test
|
||||
var errorFound bool
|
||||
for _, analysis := range analysisResults {
|
||||
for _, err := range analysis.Error {
|
||||
if strings.Contains(err.Text, "does not have resource configured."){
|
||||
if strings.Contains(err.Text, "does not have resource configured.") {
|
||||
errorFound = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type IngressAnalyzer struct{}
|
||||
@@ -26,6 +28,14 @@ type IngressAnalyzer struct{}
|
||||
func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "Ingress"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "networking",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
@@ -46,8 +56,11 @@ func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
if ingressClassName == nil {
|
||||
ingClassValue := ing.Annotations["kubernetes.io/ingress.class"]
|
||||
if ingClassValue == "" {
|
||||
doc := apiDoc.GetApiDocV2("spec.ingressClassName")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Ingress %s/%s does not specify an Ingress class.", ing.Namespace, ing.Name),
|
||||
Text: fmt.Sprintf("Ingress %s/%s does not specify an Ingress class.", ing.Namespace, ing.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: ing.Namespace,
|
||||
@@ -68,8 +81,11 @@ func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
if ingressClassName != nil {
|
||||
_, err := a.Client.GetClient().NetworkingV1().IngressClasses().Get(a.Context, *ingressClassName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
doc := apiDoc.GetApiDocV2("spec.ingressClassName")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Ingress uses the ingress class %s which does not exist.", *ingressClassName),
|
||||
Text: fmt.Sprintf("Ingress uses the ingress class %s which does not exist.", *ingressClassName),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: *ingressClassName,
|
||||
@@ -86,8 +102,11 @@ func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
for _, path := range rule.HTTP.Paths {
|
||||
_, err := a.Client.GetClient().CoreV1().Services(ing.Namespace).Get(a.Context, path.Backend.Service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
doc := apiDoc.GetApiDocV2("spec.rules.http.paths.backend.service")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Ingress uses the service %s/%s which does not exist.", ing.Namespace, path.Backend.Service.Name),
|
||||
Text: fmt.Sprintf("Ingress uses the service %s/%s which does not exist.", ing.Namespace, path.Backend.Service.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: ing.Namespace,
|
||||
@@ -106,8 +125,11 @@ func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
for _, tls := range ing.Spec.TLS {
|
||||
_, err := a.Client.GetClient().CoreV1().Secrets(ing.Namespace).Get(a.Context, tls.SecretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
doc := apiDoc.GetApiDocV2("spec.tls.secretName")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Ingress uses the secret %s/%s as a TLS certificate which does not exist.", ing.Namespace, tls.SecretName),
|
||||
Text: fmt.Sprintf("Ingress uses the secret %s/%s as a TLS certificate which does not exist.", ing.Namespace, tls.SecretName),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: ing.Namespace,
|
||||
|
||||
@@ -17,8 +17,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type NetworkPolicyAnalyzer struct{}
|
||||
@@ -26,6 +28,14 @@ type NetworkPolicyAnalyzer struct{}
|
||||
func (NetworkPolicyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "NetworkPolicy"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "networking",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
@@ -45,8 +55,11 @@ func (NetworkPolicyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
|
||||
|
||||
// Check if policy allows traffic to all pods in the namespace
|
||||
if len(policy.Spec.PodSelector.MatchLabels) == 0 {
|
||||
doc := apiDoc.GetApiDocV2("spec.podSelector.matchLabels")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Network policy allows traffic to all pods: %s", policy.Name),
|
||||
Text: fmt.Sprintf("Network policy allows traffic to all pods: %s", policy.Name),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: policy.Name,
|
||||
|
||||
@@ -58,7 +58,7 @@ func (NodeAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
preAnalysis[fmt.Sprintf("%s", node.Name)] = common.PreAnalysis{
|
||||
preAnalysis[node.Name] = common.PreAnalysis{
|
||||
Node: node,
|
||||
FailureDetails: failures,
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type PdbAnalyzer struct{}
|
||||
@@ -26,6 +28,14 @@ type PdbAnalyzer struct{}
|
||||
func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "PodDisruptionBudget"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "policy",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
@@ -49,8 +59,11 @@ func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
if evt.Reason == "NoPods" && evt.Message != "" {
|
||||
if pdb.Spec.Selector != nil {
|
||||
for k, v := range pdb.Spec.Selector.MatchLabels {
|
||||
doc := apiDoc.GetApiDocV2("spec.selector.matchLabels")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("%s, expected label %s=%s", evt.Message, k, v),
|
||||
Text: fmt.Sprintf("%s, expected label %s=%s", evt.Message, k, v),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: k,
|
||||
@@ -64,15 +77,21 @@ func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
})
|
||||
}
|
||||
for _, v := range pdb.Spec.Selector.MatchExpressions {
|
||||
doc := apiDoc.GetApiDocV2("spec.selector.matchExpressions")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("%s, expected expression %s", evt.Message, v),
|
||||
Sensitive: []common.Sensitive{},
|
||||
Text: fmt.Sprintf("%s, expected expression %s", evt.Message, v),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
doc := apiDoc.GetApiDocV2("spec.selector")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("%s, selector is nil", evt.Message),
|
||||
Sensitive: []common.Sensitive{},
|
||||
Text: fmt.Sprintf("%s, selector is nil", evt.Message),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func (PodAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
}
|
||||
} else {
|
||||
// when pod is Running but its ReadinessProbe fails
|
||||
if containerStatus.Ready == false && pod.Status.Phase == "Running" {
|
||||
if !containerStatus.Ready && pod.Status.Phase == "Running" {
|
||||
// parse the event log and append details
|
||||
evt, err := FetchLatestEvent(a.Context, a.Client, pod.Namespace, pod.Name)
|
||||
if err != nil || evt == nil {
|
||||
|
||||
@@ -18,8 +18,10 @@ import (
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type ServiceAnalyzer struct{}
|
||||
@@ -27,6 +29,14 @@ type ServiceAnalyzer struct{}
|
||||
func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "Service"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
@@ -52,8 +62,11 @@ func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
}
|
||||
|
||||
for k, v := range svc.Spec.Selector {
|
||||
doc := apiDoc.GetApiDocV2("spec.selector")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Service has no endpoints, expected label %s=%s", k, v),
|
||||
Text: fmt.Sprintf("Service has no endpoints, expected label %s=%s", k, v),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: k,
|
||||
@@ -72,14 +85,20 @@ func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
// Check through container status to check for crashes
|
||||
for _, epSubset := range ep.Subsets {
|
||||
apiDoc.Kind = "Endpoints"
|
||||
|
||||
if len(epSubset.NotReadyAddresses) > 0 {
|
||||
for _, addresses := range epSubset.NotReadyAddresses {
|
||||
count++
|
||||
pods = append(pods, addresses.TargetRef.Kind+"/"+addresses.TargetRef.Name)
|
||||
}
|
||||
|
||||
doc := apiDoc.GetApiDocV2("subsets.notReadyAddresses")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("Service has not ready endpoints, pods: %s, expected %d", pods, count),
|
||||
Sensitive: []common.Sensitive{},
|
||||
Text: fmt.Sprintf("Service has not ready endpoints, pods: %s, expected %d", pods, count),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type StatefulSetAnalyzer struct{}
|
||||
@@ -26,6 +28,14 @@ type StatefulSetAnalyzer struct{}
|
||||
func (StatefulSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
|
||||
kind := "StatefulSet"
|
||||
apiDoc := kubernetes.K8sApiReference{
|
||||
Kind: kind,
|
||||
ApiVersion: schema.GroupVersion{
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
},
|
||||
OpenapiSchema: a.OpenapiSchema,
|
||||
}
|
||||
|
||||
AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{
|
||||
"analyzer_name": kind,
|
||||
@@ -44,8 +54,15 @@ func (StatefulSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
|
||||
serviceName := sts.Spec.ServiceName
|
||||
_, err := a.Client.GetClient().CoreV1().Services(sts.Namespace).Get(a.Context, serviceName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
doc := apiDoc.GetApiDocV2("spec.serviceName")
|
||||
|
||||
failures = append(failures, common.Failure{
|
||||
Text: fmt.Sprintf("StatefulSet uses the service %s/%s which does not exist.", sts.Namespace, serviceName),
|
||||
Text: fmt.Sprintf(
|
||||
"StatefulSet uses the service %s/%s which does not exist.",
|
||||
sts.Namespace,
|
||||
serviceName,
|
||||
),
|
||||
KubernetesDoc: doc,
|
||||
Sensitive: []common.Sensitive{
|
||||
{
|
||||
Unmasked: sts.Namespace,
|
||||
|
||||
71
pkg/cache/cache.go
vendored
71
pkg/cache/cache.go
vendored
@@ -1,14 +1,83 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type ICache interface {
|
||||
Store(key string, data string) error
|
||||
Load(key string) (string, error)
|
||||
List() ([]string, error)
|
||||
Exists(key string) bool
|
||||
IsCacheDisabled() bool
|
||||
}
|
||||
|
||||
func New(noCache bool) ICache {
|
||||
func New(noCache bool, remoteCache bool) ICache {
|
||||
if remoteCache {
|
||||
return NewS3Cache(noCache)
|
||||
}
|
||||
return &FileBasedCache{
|
||||
noCache: noCache,
|
||||
}
|
||||
}
|
||||
|
||||
// CacheProvider is the configuration for the cache provider when using a remote cache
|
||||
type CacheProvider struct {
|
||||
BucketName string `mapstructure:"bucketname"`
|
||||
Region string `mapstructure:"region"`
|
||||
}
|
||||
|
||||
func RemoteCacheEnabled() (bool, error) {
|
||||
// load remote cache if it is configured
|
||||
var cache CacheProvider
|
||||
err := viper.UnmarshalKey("cache", &cache)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if cache.BucketName != "" && cache.Region != "" {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func AddRemoteCache(bucketName string, region string) error {
|
||||
var cacheInfo CacheProvider
|
||||
err := viper.UnmarshalKey("cache", &cacheInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cacheInfo.BucketName != "" {
|
||||
return errors.New("Error: a cache is already configured, please remove it first")
|
||||
}
|
||||
cacheInfo.BucketName = bucketName
|
||||
cacheInfo.Region = region
|
||||
viper.Set("cache", cacheInfo)
|
||||
err = viper.WriteConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveRemoteCache(bucketName string) error {
|
||||
var cacheInfo CacheProvider
|
||||
err := viper.UnmarshalKey("cache", &cacheInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cacheInfo.BucketName == "" {
|
||||
return errors.New("Error: no cache is configured")
|
||||
}
|
||||
|
||||
cacheInfo = CacheProvider{}
|
||||
viper.Set("cache", cacheInfo)
|
||||
err = viper.WriteConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
19
pkg/cache/file_based.go
vendored
19
pkg/cache/file_based.go
vendored
@@ -19,6 +19,25 @@ func (f *FileBasedCache) IsCacheDisabled() bool {
|
||||
return f.noCache
|
||||
}
|
||||
|
||||
func (*FileBasedCache) List() ([]string, error) {
|
||||
path, err := xdg.CacheFile("k8sgpt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, file := range files {
|
||||
result = append(result, file.Name())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (*FileBasedCache) Exists(key string) bool {
|
||||
path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
|
||||
|
||||
|
||||
118
pkg/cache/s3_based.go
vendored
Normal file
118
pkg/cache/s3_based.go
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Generate ICache implementation
|
||||
type S3Cache struct {
|
||||
noCache bool
|
||||
bucketName string
|
||||
session *s3.S3
|
||||
}
|
||||
|
||||
func (s *S3Cache) Store(key string, data string) error {
|
||||
// Store the object as a new file in the bucket with data as the content
|
||||
_, err := s.session.PutObject(&s3.PutObjectInput{
|
||||
Body: aws.ReadSeekCloser(bytes.NewReader([]byte(data))),
|
||||
Bucket: aws.String(s.bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (s *S3Cache) Load(key string) (string, error) {
|
||||
|
||||
// Retrieve the object from the bucket and load it into a string
|
||||
result, err := s.session.GetObject(&s3.GetObjectInput{
|
||||
Bucket: aws.String(s.bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(result.Body)
|
||||
result.Body.Close()
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (s *S3Cache) List() ([]string, error) {
|
||||
|
||||
// List the files in the bucket
|
||||
result, err := s.session.ListObjectsV2(&s3.ListObjectsV2Input{Bucket: aws.String(s.bucketName)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for _, item := range result.Contents {
|
||||
keys = append(keys, *item.Key)
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (s *S3Cache) Exists(key string) bool {
|
||||
// Check if the object exists in the bucket
|
||||
_, err := s.session.HeadObject(&s3.HeadObjectInput{
|
||||
Bucket: aws.String(s.bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
return err == nil
|
||||
|
||||
}
|
||||
|
||||
func (s *S3Cache) IsCacheDisabled() bool {
|
||||
return s.noCache
|
||||
}
|
||||
|
||||
func NewS3Cache(nocache bool) ICache {
|
||||
|
||||
var cache CacheProvider
|
||||
err := viper.UnmarshalKey("cache", &cache)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if cache.BucketName == "" {
|
||||
panic("Bucket name not configured")
|
||||
}
|
||||
if cache.Region == "" {
|
||||
panic("Region not configured")
|
||||
}
|
||||
|
||||
sess := session.Must(session.NewSessionWithOptions(session.Options{
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
Config: aws.Config{
|
||||
Region: aws.String(cache.Region),
|
||||
},
|
||||
}))
|
||||
|
||||
s := s3.New(sess)
|
||||
|
||||
// Check if the bucket exists, if not create it
|
||||
_, err = s.HeadBucket(&s3.HeadBucketInput{
|
||||
Bucket: aws.String(cache.BucketName),
|
||||
})
|
||||
if err != nil {
|
||||
_, err = s.CreateBucket(&s3.CreateBucketInput{
|
||||
Bucket: aws.String(cache.BucketName),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return &S3Cache{
|
||||
noCache: nocache,
|
||||
session: s,
|
||||
bucketName: cache.BucketName,
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
|
||||
trivy "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
|
||||
openapi_v2 "github.com/google/gnostic/openapiv2"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
@@ -31,12 +32,13 @@ type IAnalyzer interface {
|
||||
}
|
||||
|
||||
type Analyzer struct {
|
||||
Client *kubernetes.Client
|
||||
Context context.Context
|
||||
Namespace string
|
||||
AIClient ai.IAI
|
||||
PreAnalysis map[string]PreAnalysis
|
||||
Results []Result
|
||||
Client *kubernetes.Client
|
||||
Context context.Context
|
||||
Namespace string
|
||||
AIClient ai.IAI
|
||||
PreAnalysis map[string]PreAnalysis
|
||||
Results []Result
|
||||
OpenapiSchema *openapi_v2.Document
|
||||
}
|
||||
|
||||
type PreAnalysis struct {
|
||||
@@ -65,8 +67,9 @@ type Result struct {
|
||||
}
|
||||
|
||||
type Failure struct {
|
||||
Text string
|
||||
Sensitive []Sensitive
|
||||
Text string
|
||||
KubernetesDoc string
|
||||
Sensitive []Sensitive
|
||||
}
|
||||
|
||||
type Sensitive struct {
|
||||
|
||||
@@ -66,14 +66,11 @@ func (*Integration) Get(name string) (IIntegration, error) {
|
||||
return integrations[name], nil
|
||||
}
|
||||
|
||||
func (*Integration) Activate(name string, namespace string) error {
|
||||
func (*Integration) Activate(name string, namespace string, activeFilters []string) error {
|
||||
if _, ok := integrations[name]; !ok {
|
||||
return errors.New("integration not found")
|
||||
}
|
||||
|
||||
// Update filters
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
|
||||
mergedFilters := append(activeFilters, integrations[name].GetAnalyzerName())
|
||||
|
||||
uniqueFilters, dupplicatedFilters := util.RemoveDuplicates(mergedFilters)
|
||||
|
||||
70
pkg/kubernetes/apireference.go
Normal file
70
pkg/kubernetes/apireference.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
openapi_v2 "github.com/google/gnostic/openapiv2"
|
||||
)
|
||||
|
||||
func (k *K8sApiReference) GetApiDocV2(field string) string {
|
||||
startPoint := ""
|
||||
// the path must be formated like "path1.path2.path3"
|
||||
paths := strings.Split(field, ".")
|
||||
group := strings.Split(k.ApiVersion.Group, ".")
|
||||
definitions := k.OpenapiSchema.GetDefinitions().GetAdditionalProperties()
|
||||
|
||||
// extract the startpoint by searching the highest leaf corresponding to the requested group qnd kind
|
||||
for _, prop := range definitions {
|
||||
if strings.HasSuffix(prop.GetName(), fmt.Sprintf("%s.%s.%s", group[0], k.ApiVersion.Version, k.Kind)) {
|
||||
startPoint = prop.GetName()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// recursively parse the definitions to find the description of the latest part of the given path
|
||||
description := k.recursePath(definitions, startPoint, paths)
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
func (k *K8sApiReference) recursePath(definitions []*openapi_v2.NamedSchema, leaf string, paths []string) string {
|
||||
description := ""
|
||||
|
||||
for _, prop := range definitions {
|
||||
// search the requested leaf
|
||||
if prop.GetName() == leaf {
|
||||
for _, addProp := range prop.GetValue().GetProperties().GetAdditionalProperties() {
|
||||
// search the additional property of the leaf corresponding the current path
|
||||
if addProp.GetName() == paths[0] {
|
||||
// the last path or the path is string, we get the description and we go out
|
||||
if len(paths) == 1 || addProp.GetValue().GetType().String() == "value:\"string\"" {
|
||||
// extract the path description as we are at the end of the paths
|
||||
description = addProp.GetValue().Description
|
||||
} else {
|
||||
// the path is an object, we extract the xref
|
||||
if addProp.GetValue().GetXRef() != "" {
|
||||
splitRef := strings.Split(addProp.GetValue().GetXRef(), "/")
|
||||
reducedPaths := paths[1:]
|
||||
description = k.recursePath(definitions, splitRef[len(splitRef)-1], reducedPaths)
|
||||
}
|
||||
|
||||
// the path is an array, we take the first xref from the items
|
||||
if len(addProp.GetValue().GetItems().GetSchema()) == 1 {
|
||||
splitRef := strings.Split(addProp.GetValue().GetItems().GetSchema()[0].GetXRef(), "/")
|
||||
reducedPaths := paths[1:]
|
||||
description = k.recursePath(definitions, splitRef[len(splitRef)-1], reducedPaths)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
@@ -22,12 +22,6 @@ import (
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Client kubernetes.Interface
|
||||
RestClient rest.Interface
|
||||
Config *rest.Config
|
||||
}
|
||||
|
||||
func (c *Client) GetConfig() *rest.Config {
|
||||
return c.Config
|
||||
}
|
||||
@@ -74,9 +68,15 @@ func NewClient(kubecontext string, kubeconfig string) (*Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serverVersion, err := clientSet.ServerVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
Client: clientSet,
|
||||
RestClient: restClient,
|
||||
Config: config,
|
||||
Client: clientSet,
|
||||
RestClient: restClient,
|
||||
Config: config,
|
||||
ServerVersion: serverVersion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
22
pkg/kubernetes/types.go
Normal file
22
pkg/kubernetes/types.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
openapi_v2 "github.com/google/gnostic/openapiv2"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/version"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Client kubernetes.Interface
|
||||
RestClient rest.Interface
|
||||
Config *rest.Config
|
||||
ServerVersion *version.Info
|
||||
}
|
||||
|
||||
type K8sApiReference struct {
|
||||
ApiVersion schema.GroupVersion
|
||||
Kind string
|
||||
OpenapiSchema *openapi_v2.Document
|
||||
}
|
||||
61
pkg/server/analyze.go
Normal file
61
pkg/server/analyze.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
json "encoding/json"
|
||||
|
||||
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
|
||||
)
|
||||
|
||||
func (h *handler) Analyze(ctx context.Context, i *schemav1.AnalyzeRequest) (
|
||||
*schemav1.AnalyzeResponse,
|
||||
error,
|
||||
) {
|
||||
if i.Output == "" {
|
||||
i.Output = "json"
|
||||
}
|
||||
|
||||
if i.Backend == "" {
|
||||
i.Backend = "openai"
|
||||
}
|
||||
|
||||
if int(i.MaxConcurrency) == 0 {
|
||||
i.MaxConcurrency = 10
|
||||
}
|
||||
|
||||
config, err := analysis.NewAnalysis(
|
||||
i.Backend,
|
||||
i.Language,
|
||||
i.Filters,
|
||||
i.Namespace,
|
||||
i.Nocache,
|
||||
i.Explain,
|
||||
int(i.MaxConcurrency),
|
||||
false, // Kubernetes Doc disabled in server mode
|
||||
)
|
||||
if err != nil {
|
||||
return &schemav1.AnalyzeResponse{}, err
|
||||
}
|
||||
config.RunAnalysis()
|
||||
|
||||
if i.Explain {
|
||||
err := config.GetAIResults(i.Output, i.Anonymize)
|
||||
if err != nil {
|
||||
return &schemav1.AnalyzeResponse{}, err
|
||||
}
|
||||
}
|
||||
|
||||
out, err := config.PrintOutput(i.Output)
|
||||
if err != nil {
|
||||
return &schemav1.AnalyzeResponse{}, err
|
||||
}
|
||||
var obj schemav1.AnalyzeResponse
|
||||
|
||||
err = json.Unmarshal(out, &obj)
|
||||
if err != nil {
|
||||
return &schemav1.AnalyzeResponse{}, err
|
||||
}
|
||||
|
||||
return &obj, nil
|
||||
}
|
||||
37
pkg/server/config.go
Normal file
37
pkg/server/config.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
)
|
||||
|
||||
func (h *handler) AddConfig(ctx context.Context, i *schemav1.AddConfigRequest) (*schemav1.AddConfigResponse, error,
|
||||
) {
|
||||
if i.Cache.BucketName == "" || i.Cache.Region == "" {
|
||||
return nil, errors.New("BucketName & Region are required")
|
||||
}
|
||||
|
||||
err := cache.AddRemoteCache(i.Cache.BucketName, i.Cache.Region)
|
||||
if err != nil {
|
||||
return &schemav1.AddConfigResponse{}, err
|
||||
}
|
||||
|
||||
return &schemav1.AddConfigResponse{
|
||||
Status: "Configuration updated.",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *handler) RemoveConfig(ctx context.Context, i *schemav1.RemoveConfigRequest) (*schemav1.RemoveConfigResponse, error,
|
||||
) {
|
||||
err := cache.RemoveRemoteCache(i.Cache.BucketName)
|
||||
if err != nil {
|
||||
return &schemav1.RemoveConfigResponse{}, err
|
||||
}
|
||||
|
||||
return &schemav1.RemoveConfigResponse{
|
||||
Status: "Successfully removed the remote cache",
|
||||
}, nil
|
||||
}
|
||||
9
pkg/server/handler.go
Normal file
9
pkg/server/handler.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
rpc.UnimplementedServerServiceServer
|
||||
}
|
||||
@@ -1,77 +1,52 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type loggingResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
func logInterceptor(logger *zap.Logger) grpc.UnaryServerInterceptor {
|
||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
start := time.Now()
|
||||
|
||||
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
|
||||
return &loggingResponseWriter{
|
||||
w,
|
||||
http.StatusOK,
|
||||
&bytes.Buffer{},
|
||||
// Call the handler to execute the gRPC request
|
||||
response, err := handler(ctx, req)
|
||||
|
||||
duration := time.Since(start).Milliseconds()
|
||||
fields := []zap.Field{
|
||||
zap.Int64("duration_ms", duration),
|
||||
zap.String("method", info.FullMethod),
|
||||
zap.Any("request", req),
|
||||
}
|
||||
// Get the remote address from the context
|
||||
peer, ok := peer.FromContext(ctx)
|
||||
if ok {
|
||||
fields = append(fields, zap.String("remote_addr", peer.Addr.String()))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fields = append(fields, zap.Int32("status_code", int32(status.Code(err))))
|
||||
}
|
||||
message := "request completed"
|
||||
if err != nil {
|
||||
message = fmt.Sprintf("request failed. %s", err.Error())
|
||||
}
|
||||
logRequest(logger, fields, int(status.Code(err)), message)
|
||||
|
||||
return response, err
|
||||
}
|
||||
}
|
||||
|
||||
func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
||||
lrw.statusCode = code
|
||||
lrw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
|
||||
return lrw.buf.Write(b)
|
||||
}
|
||||
|
||||
func (lrw *loggingResponseWriter) Flush() {
|
||||
if f, ok := lrw.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
lrw.ResponseWriter.Write(lrw.buf.Bytes())
|
||||
}
|
||||
|
||||
func logRequest(logger *zap.Logger, fields []zap.Field, statusCode int, message string) {
|
||||
if statusCode >= 400 {
|
||||
logger.Error(message, fields...)
|
||||
} else {
|
||||
logger.Info("request completed", fields...)
|
||||
logger.Info(message, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
config := zap.NewProductionConfig()
|
||||
config.DisableCaller = true
|
||||
logger, err := config.Build()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
lrw := NewLoggingResponseWriter(w)
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
duration := time.Since(start).Milliseconds()
|
||||
fields := []zap.Field{
|
||||
zap.Int64("duration_ms", duration),
|
||||
zap.String("method", r.Method),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.Int("status_code", lrw.statusCode),
|
||||
zap.String("url", r.URL.Path),
|
||||
}
|
||||
logRequest(logger, fields, lrw.statusCode, lrw.buf.String())
|
||||
}()
|
||||
|
||||
next.ServeHTTP(lrw, r)
|
||||
|
||||
lrw.Flush()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,23 +15,33 @@ package server
|
||||
|
||||
import (
|
||||
json "encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
|
||||
rpc "buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go/schema/v1/schemav1grpc"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
MetricsPort string
|
||||
Backend string
|
||||
Key string
|
||||
Token string
|
||||
Output string
|
||||
maxConcurrency int
|
||||
Handler *handler
|
||||
Logger *zap.Logger
|
||||
metricsServer *http.Server
|
||||
}
|
||||
|
||||
type Health struct {
|
||||
@@ -46,66 +56,46 @@ var health = Health{
|
||||
Failure: 0,
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Analysis []analysis.Analysis `json:"analysis"`
|
||||
}
|
||||
|
||||
func (s *Config) analyzeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
namespace := r.URL.Query().Get("namespace")
|
||||
explain := getBoolParam(r.URL.Query().Get("explain"))
|
||||
anonymize := getBoolParam(r.URL.Query().Get("anonymize"))
|
||||
nocache := getBoolParam(r.URL.Query().Get("nocache"))
|
||||
language := r.URL.Query().Get("language")
|
||||
|
||||
var err error
|
||||
s.maxConcurrency, err = strconv.Atoi(r.URL.Query().Get("maxConcurrency"))
|
||||
if err != nil {
|
||||
s.maxConcurrency = 10
|
||||
}
|
||||
s.Output = r.URL.Query().Get("output")
|
||||
|
||||
if s.Output == "" {
|
||||
s.Output = "json"
|
||||
}
|
||||
|
||||
config, err := analysis.NewAnalysis(s.Backend, language, []string{}, namespace, nocache, explain, s.maxConcurrency)
|
||||
if err != nil {
|
||||
health.Failure++
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
config.RunAnalysis()
|
||||
|
||||
if explain {
|
||||
err := config.GetAIResults(s.Output, anonymize)
|
||||
if err != nil {
|
||||
health.Failure++
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
out, err := config.PrintOutput(s.Output)
|
||||
if err != nil {
|
||||
health.Failure++
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
health.Success++
|
||||
fmt.Fprintf(w, string(out))
|
||||
}
|
||||
|
||||
func (s *Config) Serve() error {
|
||||
handler := loggingMiddleware(http.DefaultServeMux)
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
http.HandleFunc("/analyze", s.analyzeHandler)
|
||||
http.HandleFunc("/healthz", s.healthzHandler)
|
||||
color.Green("Starting server on port %s", s.Port)
|
||||
err := http.ListenAndServe(":"+s.Port, handler)
|
||||
|
||||
var lis net.Listener
|
||||
var err error
|
||||
address := fmt.Sprintf(":%s", s.Port)
|
||||
lis, err = net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
fmt.Printf("error starting server: %s\n", err)
|
||||
return err
|
||||
}
|
||||
s.Logger.Info(fmt.Sprintf("binding api to %s", s.Port))
|
||||
grpcServerUnaryInterceptor := grpc.UnaryInterceptor(logInterceptor(s.Logger))
|
||||
grpcServer := grpc.NewServer(grpcServerUnaryInterceptor)
|
||||
reflection.Register(grpcServer)
|
||||
rpc.RegisterServerServiceServer(grpcServer, s.Handler)
|
||||
if err := grpcServer.Serve(
|
||||
lis,
|
||||
); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Config) ServeMetrics() error {
|
||||
s.Logger.Info(fmt.Sprintf("binding metrics to %s", s.MetricsPort))
|
||||
s.metricsServer = &http.Server{
|
||||
ReadHeaderTimeout: 3 * time.Second,
|
||||
Addr: fmt.Sprintf(":%s", s.MetricsPort),
|
||||
}
|
||||
s.metricsServer.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/healthz":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
case "/metrics":
|
||||
promhttp.Handler().ServeHTTP(w, r)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
if err := s.metricsServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -117,7 +107,7 @@ func (s *Config) healthzHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, string(js))
|
||||
fmt.Fprint(w, string(js))
|
||||
}
|
||||
|
||||
func getBoolParam(param string) bool {
|
||||
|
||||
Reference in New Issue
Block a user