mirror of
https://github.com/k8sgpt-ai/k8sgpt.git
synced 2026-03-19 11:33:08 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96d86d3eb0 | ||
|
|
df17e3e728 | ||
|
|
80904e3063 | ||
|
|
cf6f9289e1 | ||
|
|
a79224e2bf | ||
|
|
9ce33469d8 | ||
|
|
969fe99b33 | ||
|
|
91d423b147 |
@@ -1 +1 @@
|
||||
{".":"0.4.10"}
|
||||
{".":"0.4.11"}
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## [0.4.11](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.10...v0.4.11) (2025-04-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add verbose flag to enable detailed output ([#1420](https://github.com/k8sgpt-ai/k8sgpt/issues/1420)) ([a79224e](https://github.com/k8sgpt-ai/k8sgpt/commit/a79224e2bf96f458dbc96404c8f4847970e8d2ef))
|
||||
* call bedrock with inference profile ([#1449](https://github.com/k8sgpt-ai/k8sgpt/issues/1449)) ([91d423b](https://github.com/k8sgpt-ai/k8sgpt/commit/91d423b147ca18cda7d54ff19349938a894ecb85))
|
||||
* improved test coverage ([#1455](https://github.com/k8sgpt-ai/k8sgpt/issues/1455)) ([80904e3](https://github.com/k8sgpt-ai/k8sgpt/commit/80904e3063b00b0536171b7b62b938938b20825a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* config ai provider in query ([#1457](https://github.com/k8sgpt-ai/k8sgpt/issues/1457)) ([df17e3e](https://github.com/k8sgpt-ai/k8sgpt/commit/df17e3e728591e974703527dff86de882af17790))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1447](https://github.com/k8sgpt-ai/k8sgpt/issues/1447)) ([969fe99](https://github.com/k8sgpt-ai/k8sgpt/commit/969fe99b3320c313f1c97133cdffb668a00d5fb5))
|
||||
* **deps:** update module gopkg.in/yaml.v2 to v3 ([#1453](https://github.com/k8sgpt-ai/k8sgpt/issues/1453)) ([cf6f928](https://github.com/k8sgpt-ai/k8sgpt/commit/cf6f9289e13ee729c24968fd771c901f412e8db7))
|
||||
|
||||
|
||||
### Docs
|
||||
|
||||
* fix the slack invite link ([#1450](https://github.com/k8sgpt-ai/k8sgpt/issues/1450)) ([9ce3346](https://github.com/k8sgpt-ai/k8sgpt/commit/9ce33469d85aa0829e995e4b404ae85734124fb4))
|
||||
|
||||
## [0.4.10](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.4.9...v0.4.10) (2025-04-10)
|
||||
|
||||
|
||||
|
||||
16
README.md
16
README.md
@@ -62,7 +62,7 @@ brew install k8sgpt
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.10/k8sgpt_386.rpm
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.11/k8sgpt_386.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
|
||||
@@ -70,7 +70,7 @@ brew install k8sgpt
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.10/k8sgpt_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.11/k8sgpt_amd64.rpm
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
</details>
|
||||
@@ -83,7 +83,7 @@ brew install k8sgpt
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.10/k8sgpt_386.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.11/k8sgpt_386.deb
|
||||
sudo dpkg -i k8sgpt_386.deb
|
||||
```
|
||||
|
||||
@@ -94,7 +94,7 @@ sudo dpkg -i k8sgpt_386.deb
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
```
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.10/k8sgpt_amd64.deb
|
||||
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.11/k8sgpt_amd64.deb
|
||||
sudo dpkg -i k8sgpt_amd64.deb
|
||||
```
|
||||
|
||||
@@ -109,7 +109,7 @@ sudo dpkg -i k8sgpt_amd64.deb
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.10/k8sgpt_386.apk
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.11/k8sgpt_386.apk
|
||||
apk add --allow-untrusted k8sgpt_386.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -118,7 +118,7 @@ sudo dpkg -i k8sgpt_amd64.deb
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
```
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.10/k8sgpt_amd64.apk
|
||||
wget https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.4.11/k8sgpt_amd64.apk
|
||||
apk add --allow-untrusted k8sgpt_amd64.apk
|
||||
```
|
||||
<!---x-release-please-end-->
|
||||
@@ -592,7 +592,7 @@ Please read our [contributing guide](./CONTRIBUTING.md).
|
||||
|
||||
## Community
|
||||
|
||||
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-276pa9uyq-pxAUr4TCVHubFxEvLZuT1Q)
|
||||
Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-332vhyaxv-bfjJwHZLXWVCB3QaXafEYQ)
|
||||
|
||||
<a href="https://github.com/k8sgpt-ai/k8sgpt/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=k8sgpt-ai/k8sgpt" />
|
||||
@@ -600,4 +600,4 @@ Find us on [Slack](https://join.slack.com/t/k8sgpt/shared_invite/zt-276pa9uyq-px
|
||||
|
||||
## License
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fk8sgpt-ai%2Fk8sgpt?ref=badge_large)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fk8sgpt-ai%2Fk8sgpt?ref=badge_large)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai/interactive"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analysis"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -67,25 +68,45 @@ var AnalyzeCmd = &cobra.Command{
|
||||
withStats,
|
||||
)
|
||||
|
||||
verbose := viper.GetBool("verbose")
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking analysis configuration.")
|
||||
}
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Println("Debug: Analysis initialized.")
|
||||
}
|
||||
defer config.Close()
|
||||
|
||||
if customAnalysis {
|
||||
config.RunCustomAnalysis()
|
||||
if verbose {
|
||||
fmt.Println("Debug: All custom analyzers completed.")
|
||||
}
|
||||
}
|
||||
config.RunAnalysis()
|
||||
if verbose {
|
||||
fmt.Println("Debug: All core analyzers completed.")
|
||||
}
|
||||
|
||||
if explain {
|
||||
if err := config.GetAIResults(output, anonymize); err != nil {
|
||||
err := config.GetAIResults(output, anonymize)
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking AI results.")
|
||||
}
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
// print results
|
||||
output_data, err := config.PrintOutput(output)
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking output.")
|
||||
}
|
||||
if err != nil {
|
||||
color.Red("Error: %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -37,6 +37,7 @@ var (
|
||||
cfgFile string
|
||||
kubecontext string
|
||||
kubeconfig string
|
||||
verbose bool
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
@@ -84,6 +85,7 @@ func init() {
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", fmt.Sprintf("Default config file (%s/k8sgpt/k8sgpt.yaml)", xdg.ConfigHome))
|
||||
rootCmd.PersistentFlags().StringVar(&kubecontext, "kubecontext", "", "Kubernetes context to use. Only required if out-of-cluster.")
|
||||
rootCmd.PersistentFlags().StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show detailed tool actions (e.g., API calls, checks).")
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
@@ -104,6 +106,7 @@ func initConfig() {
|
||||
|
||||
viper.Set("kubecontext", kubecontext)
|
||||
viper.Set("kubeconfig", kubeconfig)
|
||||
viper.Set("verbose", verbose)
|
||||
|
||||
viper.SetEnvPrefix("K8SGPT")
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
30
cmd/root_test.go
Normal file
30
cmd/root_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Test that verbose flag is correctly set in viper.
|
||||
func TestInitConfig_VerboseFlag(t *testing.T) {
|
||||
verbose = true
|
||||
viper.Reset()
|
||||
initConfig()
|
||||
if !viper.GetBool("verbose") {
|
||||
t.Error("Expected verbose flag to be true")
|
||||
}
|
||||
}
|
||||
4
go.mod
4
go.mod
@@ -35,6 +35,7 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.5.0
|
||||
github.com/IBM/watsonx-go v1.0.1
|
||||
github.com/agiledragon/gomonkey/v2 v2.13.0
|
||||
github.com/aws/aws-sdk-go v1.55.6
|
||||
github.com/cohere-ai/cohere-go/v2 v2.12.2
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
@@ -48,6 +49,7 @@ require (
|
||||
github.com/pterm/pterm v0.12.80
|
||||
google.golang.org/api v0.218.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
sigs.k8s.io/controller-runtime v0.19.3
|
||||
sigs.k8s.io/gateway-api v1.2.1
|
||||
)
|
||||
@@ -120,6 +122,7 @@ require (
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/sony/gobreaker v0.5.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
@@ -135,7 +138,6 @@ require (
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
knative.dev/pkg v0.0.0-20241026180704-25f6002b00f3 // indirect
|
||||
)
|
||||
|
||||
|
||||
7
go.sum
7
go.sum
@@ -713,6 +713,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/agiledragon/gomonkey/v2 v2.13.0 h1:B24Jg6wBI1iB8EFR1c+/aoTg7QN/Cum7YffG8KMIyYo=
|
||||
github.com/agiledragon/gomonkey/v2 v2.13.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
|
||||
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
@@ -1075,6 +1077,7 @@ github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/Q
|
||||
github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw=
|
||||
github.com/gophercloud/gophercloud/v2 v2.4.0 h1:XhP5tVEH3ni66NSNK1+0iSO6kaGPH/6srtx6Cr+8eCg=
|
||||
github.com/gophercloud/gophercloud/v2 v2.4.0/go.mod h1:uJWNpTgJPSl2gyzJqcU/pIAhFUWvIkp8eE8M15n9rs4=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
@@ -1152,6 +1155,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
@@ -1382,6 +1386,8 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
|
||||
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
@@ -1802,6 +1808,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/bedrockruntime/bedrockruntimeiface"
|
||||
@@ -293,6 +294,11 @@ func (a *AmazonBedRockClient) getModelFromString(model string) (*bedrock_support
|
||||
strings.Contains(modelConfigNameLower, modelLower) || strings.Contains(modelLower, modelConfigNameLower) {
|
||||
// Create a copy to avoid returning a pointer to a loop variable
|
||||
modelCopy := a.models[i]
|
||||
// for partial match, set the model name to the input string if it is a valid ARN
|
||||
if validateModelArn(modelLower) {
|
||||
modelCopy.Config.ModelName = modelLower
|
||||
}
|
||||
|
||||
return &modelCopy, nil
|
||||
}
|
||||
}
|
||||
@@ -300,6 +306,11 @@ func (a *AmazonBedRockClient) getModelFromString(model string) (*bedrock_support
|
||||
return nil, fmt.Errorf("model '%s' not found in supported models", model)
|
||||
}
|
||||
|
||||
func validateModelArn(model string) bool {
|
||||
var re = regexp.MustCompile(`(?m)^arn:(?P<Partition>[^:\n]*):bedrock:(?P<Region>[^:\n]*):(?P<AccountID>[^:\n]*):(?P<Ignore>(?P<ResourceType>[^:\/\n]*)[:\/])?(?P<Resource>.*)$`)
|
||||
return re.MatchString(model)
|
||||
}
|
||||
|
||||
// Configure configures the AmazonBedRockClient with the provided configuration.
|
||||
func (a *AmazonBedRockClient) Configure(config IAIConfig) error {
|
||||
// Initialize models if not already initialized
|
||||
|
||||
@@ -41,7 +41,15 @@ func TestBedrockModelConfig(t *testing.T) {
|
||||
assert.Equal(t, foundModel.Config.MaxTokens, 100)
|
||||
assert.Equal(t, foundModel.Config.Temperature, float32(0.5))
|
||||
assert.Equal(t, foundModel.Config.TopP, float32(0.9))
|
||||
assert.Equal(t, foundModel.Config.ModelName, "anthropic.claude-3-5-sonnet-20240620-v1:0")
|
||||
assert.Equal(t, foundModel.Config.ModelName, "arn:aws:bedrock:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0")
|
||||
}
|
||||
|
||||
func TestBedrockInvalidModel(t *testing.T) {
|
||||
client := &AmazonBedRockClient{models: testModels}
|
||||
|
||||
foundModel, err := client.getModelFromString("arn:aws:s3:us-east-1:*:inference-policy/anthropic.claude-3-5-sonnet-20240620-v1:0")
|
||||
assert.Nil(t, err, "Error should be nil")
|
||||
assert.Equal(t, foundModel.Config.MaxTokens, 100)
|
||||
}
|
||||
|
||||
func TestGetModelFromString(t *testing.T) {
|
||||
|
||||
87
pkg/ai/factory.go
Normal file
87
pkg/ai/factory.go
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2023 The K8sGPT Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// AIClientFactory is an interface for creating AI clients
|
||||
type AIClientFactory interface {
|
||||
NewClient(provider string) IAI
|
||||
}
|
||||
|
||||
// DefaultAIClientFactory is the default implementation of AIClientFactory
|
||||
type DefaultAIClientFactory struct{}
|
||||
|
||||
// NewClient creates a new AI client using the default implementation
|
||||
func (f *DefaultAIClientFactory) NewClient(provider string) IAI {
|
||||
return NewClient(provider)
|
||||
}
|
||||
|
||||
// ConfigProvider is an interface for accessing configuration
|
||||
type ConfigProvider interface {
|
||||
UnmarshalKey(key string, rawVal interface{}) error
|
||||
}
|
||||
|
||||
// ViperConfigProvider is the default implementation of ConfigProvider using Viper
|
||||
type ViperConfigProvider struct{}
|
||||
|
||||
// UnmarshalKey unmarshals a key from the configuration using Viper
|
||||
func (p *ViperConfigProvider) UnmarshalKey(key string, rawVal interface{}) error {
|
||||
return viper.UnmarshalKey(key, rawVal)
|
||||
}
|
||||
|
||||
// Default instances to be used
|
||||
var (
|
||||
DefaultClientFactory = &DefaultAIClientFactory{}
|
||||
DefaultConfigProvider = &ViperConfigProvider{}
|
||||
)
|
||||
|
||||
// For testing - these variables can be overridden in tests
|
||||
var (
|
||||
testAIClientFactory AIClientFactory = nil
|
||||
testConfigProvider ConfigProvider = nil
|
||||
)
|
||||
|
||||
// GetAIClientFactory returns the test factory if set, otherwise the default
|
||||
func GetAIClientFactory() AIClientFactory {
|
||||
if testAIClientFactory != nil {
|
||||
return testAIClientFactory
|
||||
}
|
||||
return DefaultClientFactory
|
||||
}
|
||||
|
||||
// GetConfigProvider returns the test provider if set, otherwise the default
|
||||
func GetConfigProvider() ConfigProvider {
|
||||
if testConfigProvider != nil {
|
||||
return testConfigProvider
|
||||
}
|
||||
return DefaultConfigProvider
|
||||
}
|
||||
|
||||
// For testing - set the test implementations
|
||||
func SetTestAIClientFactory(factory AIClientFactory) {
|
||||
testAIClientFactory = factory
|
||||
}
|
||||
|
||||
func SetTestConfigProvider(provider ConfigProvider) {
|
||||
testConfigProvider = provider
|
||||
}
|
||||
|
||||
// Reset test implementations
|
||||
func ResetTestImplementations() {
|
||||
testAIClientFactory = nil
|
||||
testConfigProvider = nil
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -89,19 +90,35 @@ func NewAnalysis(
|
||||
// Get kubernetes client from viper.
|
||||
kubecontext := viper.GetString("kubecontext")
|
||||
kubeconfig := viper.GetString("kubeconfig")
|
||||
verbose := viper.GetBool("verbose")
|
||||
client, err := kubernetes.NewClient(kubecontext, kubeconfig)
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking kubernetes client initialization.")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initialising kubernetes client: %w", err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Kubernetes client initialized, server=%s.\n", client.Config.Host)
|
||||
}
|
||||
|
||||
// Load remote cache if it is configured.
|
||||
cache, err := cache.GetCacheConfiguration()
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking cache configuration.")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Cache configuration loaded, type=%s.\n", cache.GetName())
|
||||
}
|
||||
|
||||
if noCache {
|
||||
cache.DisableCache()
|
||||
if verbose {
|
||||
fmt.Println("Debug: Cache disabled.")
|
||||
}
|
||||
}
|
||||
|
||||
a := &Analysis{
|
||||
@@ -117,12 +134,31 @@ func NewAnalysis(
|
||||
WithDoc: withDoc,
|
||||
WithStats: withStats,
|
||||
}
|
||||
if verbose {
|
||||
fmt.Print("Debug: Analysis configuration loaded, ")
|
||||
fmt.Printf("filters=%v, language=%s, ", filters, language)
|
||||
if namespace == "" {
|
||||
fmt.Printf("namespace=none, ")
|
||||
} else {
|
||||
fmt.Printf("namespace=%s, ", namespace)
|
||||
}
|
||||
if labelSelector == "" {
|
||||
fmt.Printf("labelSelector=none, ")
|
||||
} else {
|
||||
fmt.Printf("labelSelector=%s, ", labelSelector)
|
||||
}
|
||||
fmt.Printf("explain=%t, maxConcurrency=%d, ", explain, maxConcurrency)
|
||||
fmt.Printf("withDoc=%t, withStats=%t.\n", withDoc, withStats)
|
||||
}
|
||||
if !explain {
|
||||
// Return early if AI use was not requested.
|
||||
return a, nil
|
||||
}
|
||||
|
||||
var configAI ai.AIConfiguration
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking AI configuration.")
|
||||
}
|
||||
if err := viper.UnmarshalKey("ai", &configAI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -135,10 +171,16 @@ func NewAnalysis(
|
||||
// Hence, use the default provider only if the backend is not specified by the user.
|
||||
if configAI.DefaultProvider != "" && backend == "" {
|
||||
backend = configAI.DefaultProvider
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Using default AI provider %s.\n", backend)
|
||||
}
|
||||
}
|
||||
|
||||
if backend == "" {
|
||||
backend = "openai"
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Using default AI provider %s.\n", backend)
|
||||
}
|
||||
}
|
||||
|
||||
var aiProvider ai.AIProvider
|
||||
@@ -153,12 +195,23 @@ func NewAnalysis(
|
||||
return nil, fmt.Errorf("AI provider %s not specified in configuration. Please run k8sgpt auth", backend)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Debug: AI configuration loaded, provider=%s, ", backend)
|
||||
fmt.Printf("baseUrl=%s, model=%s.\n", aiProvider.BaseURL, aiProvider.Model)
|
||||
}
|
||||
|
||||
aiClient := ai.NewClient(aiProvider.Name)
|
||||
customHeaders := util.NewHeaders(httpHeaders)
|
||||
aiProvider.CustomHeaders = customHeaders
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking AI client initialization.")
|
||||
}
|
||||
if err := aiClient.Configure(&aiProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if verbose {
|
||||
fmt.Println("Debug: AI client initialized.")
|
||||
}
|
||||
a.AIClient = aiClient
|
||||
a.AnalysisAIProvider = aiProvider.Name
|
||||
return a, nil
|
||||
@@ -182,6 +235,18 @@ func (a *Analysis) RunCustomAnalysis() {
|
||||
semaphore := make(chan struct{}, a.MaxConcurrency)
|
||||
var wg sync.WaitGroup
|
||||
var mutex sync.Mutex
|
||||
verbose := viper.GetBool("verbose")
|
||||
if verbose {
|
||||
if len(customAnalyzers) == 0 {
|
||||
fmt.Println("Debug: No custom analyzers found.")
|
||||
} else {
|
||||
cAnalyzerNames := make([]string, len(customAnalyzers))
|
||||
for i, cAnalyzer := range customAnalyzers {
|
||||
cAnalyzerNames[i] = cAnalyzer.Name
|
||||
}
|
||||
fmt.Printf("Debug: Found custom analyzers %v.\n", cAnalyzerNames)
|
||||
}
|
||||
}
|
||||
for _, cAnalyzer := range customAnalyzers {
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
@@ -194,6 +259,9 @@ func (a *Analysis) RunCustomAnalysis() {
|
||||
mutex.Unlock()
|
||||
return
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s launched.\n", cAnalyzer.Name)
|
||||
}
|
||||
|
||||
result, err := canClient.Run()
|
||||
if result.Kind == "" {
|
||||
@@ -206,10 +274,16 @@ func (a *Analysis) RunCustomAnalysis() {
|
||||
mutex.Lock()
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", cAnalyzer.Name, err))
|
||||
mutex.Unlock()
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s completed with errors.\n", cAnalyzer.Name)
|
||||
}
|
||||
} else {
|
||||
mutex.Lock()
|
||||
a.Results = append(a.Results, result)
|
||||
mutex.Unlock()
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s completed without errors.\n", cAnalyzer.Name)
|
||||
}
|
||||
}
|
||||
<-semaphore
|
||||
}(cAnalyzer, &wg, semaphore)
|
||||
@@ -219,6 +293,7 @@ func (a *Analysis) RunCustomAnalysis() {
|
||||
|
||||
func (a *Analysis) RunAnalysis() {
|
||||
activeFilters := viper.GetStringSlice("active_filters")
|
||||
verbose := viper.GetBool("verbose")
|
||||
|
||||
coreAnalyzerMap, analyzerMap := analyzer.GetAnalyzerMap()
|
||||
|
||||
@@ -227,7 +302,13 @@ func (a *Analysis) RunAnalysis() {
|
||||
if a.WithDoc {
|
||||
var openApiErr error
|
||||
|
||||
if verbose {
|
||||
fmt.Println("Debug: Fetching Kubernetes docs.")
|
||||
}
|
||||
openapiSchema, openApiErr = a.Client.Client.Discovery().OpenAPISchema()
|
||||
if verbose {
|
||||
fmt.Println("Debug: Checking Kubernetes docs.")
|
||||
}
|
||||
if openApiErr != nil {
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[KubernetesDoc] %s", openApiErr))
|
||||
}
|
||||
@@ -247,6 +328,9 @@ func (a *Analysis) RunAnalysis() {
|
||||
var mutex sync.Mutex
|
||||
// if there are no filters selected and no active_filters then run coreAnalyzer
|
||||
if len(a.Filters) == 0 && len(activeFilters) == 0 {
|
||||
if verbose {
|
||||
fmt.Println("Debug: No filters selected and no active filters found, run all core analyzers.")
|
||||
}
|
||||
for name, analyzer := range coreAnalyzerMap {
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
@@ -258,6 +342,9 @@ func (a *Analysis) RunAnalysis() {
|
||||
}
|
||||
// if the filters flag is specified
|
||||
if len(a.Filters) != 0 {
|
||||
if verbose {
|
||||
fmt.Printf("Debug: Filter flags %v specified, run selected core analyzers.\n", a.Filters)
|
||||
}
|
||||
for _, filter := range a.Filters {
|
||||
if analyzer, ok := analyzerMap[filter]; ok {
|
||||
semaphore <- struct{}{}
|
||||
@@ -272,6 +359,9 @@ func (a *Analysis) RunAnalysis() {
|
||||
}
|
||||
|
||||
// use active_filters
|
||||
if len(activeFilters) > 0 && verbose {
|
||||
fmt.Printf("Debug: Found active filters %v, run selected core analyzers.\n", activeFilters)
|
||||
}
|
||||
for _, filter := range activeFilters {
|
||||
if analyzer, ok := analyzerMap[filter]; ok {
|
||||
semaphore <- struct{}{}
|
||||
@@ -294,6 +384,10 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana
|
||||
}
|
||||
|
||||
// Run the analyzer
|
||||
verbose := viper.GetBool("verbose")
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s launched.\n", reflect.TypeOf(analyzer).Name())
|
||||
}
|
||||
results, err := analyzer.Analyze(analyzerConfig)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -315,11 +409,17 @@ func (a *Analysis) executeAnalyzer(analyzer common.IAnalyzer, filter string, ana
|
||||
a.Stats = append(a.Stats, stat)
|
||||
}
|
||||
a.Errors = append(a.Errors, fmt.Sprintf("[%s] %s", filter, err))
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s completed with errors.\n", reflect.TypeOf(analyzer).Name())
|
||||
}
|
||||
} else {
|
||||
if a.WithStats {
|
||||
a.Stats = append(a.Stats, stat)
|
||||
}
|
||||
a.Results = append(a.Results, results...)
|
||||
if verbose {
|
||||
fmt.Printf("Debug: %s completed without errors.\n", reflect.TypeOf(analyzer).Name())
|
||||
}
|
||||
}
|
||||
<-semaphore
|
||||
}
|
||||
@@ -329,6 +429,11 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
verbose := viper.GetBool("verbose")
|
||||
if verbose {
|
||||
fmt.Println("Debug: Generating AI analysis.")
|
||||
}
|
||||
|
||||
var bar *progressbar.ProgressBar
|
||||
if output != "json" {
|
||||
bar = progressbar.Default(int64(len(a.Results)))
|
||||
@@ -337,6 +442,10 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
|
||||
for index, analysis := range a.Results {
|
||||
var texts []string
|
||||
|
||||
if bar != nil && verbose {
|
||||
bar.Describe(fmt.Sprintf("Analyzing %s", analysis.Kind))
|
||||
}
|
||||
|
||||
for _, failure := range analysis.Error {
|
||||
if anonymize {
|
||||
for _, s := range failure.Sensitive {
|
||||
|
||||
@@ -17,13 +17,17 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/agiledragon/gomonkey/v2"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -31,9 +35,15 @@ import (
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// sub-function
|
||||
// helper function: get type name of an analyzer
|
||||
func getTypeName(i interface{}) string {
|
||||
return reflect.TypeOf(i).Name()
|
||||
}
|
||||
|
||||
// helper function: run analysis with filter
|
||||
func analysis_RunAnalysisFilterTester(t *testing.T, filterFlag string) []common.Result {
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&v1.Pod{
|
||||
@@ -404,3 +414,252 @@ func TestGetAIResultForSanitizedFailures(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in NewAnalysis with explain=false
|
||||
func TestVerbose_NewAnalysisWithoutExplain(t *testing.T) {
|
||||
// Set viper config.
|
||||
viper.Set("verbose", true)
|
||||
viper.Set("kubecontext", "dummy")
|
||||
viper.Set("kubeconfig", "dummy")
|
||||
|
||||
// Patch kubernetes.NewClient to return a dummy client.
|
||||
patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) {
|
||||
return &kubernetes.Client{
|
||||
Config: &rest.Config{Host: "fake-server"},
|
||||
}, nil
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
output := util.CaptureOutput(func() {
|
||||
a, err := NewAnalysis(
|
||||
"", "english", []string{"Pod"}, "default", "", true,
|
||||
false, // explain
|
||||
10, false, false, []string{}, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
a.Close()
|
||||
})
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Checking kubernetes client initialization.",
|
||||
"Debug: Kubernetes client initialized, server=fake-server.",
|
||||
"Debug: Checking cache configuration.",
|
||||
"Debug: Cache configuration loaded, type=file.",
|
||||
"Debug: Cache disabled.",
|
||||
"Debug: Analysis configuration loaded, filters=[Pod], language=english, namespace=default, labelSelector=none, explain=false, maxConcurrency=10, withDoc=false, withStats=false.",
|
||||
}
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in NewAnalysis with explain=true
|
||||
func TestVerbose_NewAnalysisWithExplain(t *testing.T) {
|
||||
// Set viper config.
|
||||
viper.Set("verbose", true)
|
||||
viper.Set("kubecontext", "dummy")
|
||||
viper.Set("kubeconfig", "dummy")
|
||||
// Set a dummy AI configuration.
|
||||
dummyAIConfig := map[string]interface{}{
|
||||
"defaultProvider": "dummy",
|
||||
"providers": []map[string]interface{}{
|
||||
{
|
||||
"name": "dummy",
|
||||
"baseUrl": "http://dummy",
|
||||
"model": "dummy-model",
|
||||
"customHeaders": map[string]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
viper.Set("ai", dummyAIConfig)
|
||||
|
||||
// Patch kubernetes.NewClient to return a dummy client.
|
||||
patches := gomonkey.ApplyFunc(kubernetes.NewClient, func(kubecontext, kubeconfig string) (*kubernetes.Client, error) {
|
||||
return &kubernetes.Client{
|
||||
Config: &rest.Config{Host: "fake-server"},
|
||||
}, nil
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
// Patch ai.NewClient to return a NoOp client.
|
||||
patches2 := gomonkey.ApplyFunc(ai.NewClient, func(name string) ai.IAI {
|
||||
return &ai.NoOpAIClient{}
|
||||
})
|
||||
defer patches2.Reset()
|
||||
|
||||
output := util.CaptureOutput(func() {
|
||||
a, err := NewAnalysis(
|
||||
"", "english", []string{"Pod"}, "default", "", true,
|
||||
true, // explain
|
||||
10, false, false, []string{}, false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
a.Close()
|
||||
})
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Checking AI configuration.",
|
||||
"Debug: Using default AI provider dummy.",
|
||||
"Debug: AI configuration loaded, provider=dummy, baseUrl=http://dummy, model=dummy-model.",
|
||||
"Debug: Checking AI client initialization.",
|
||||
"Debug: AI client initialized.",
|
||||
}
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunAnalysis with filter flag
|
||||
func TestVerbose_RunAnalysisWithFilter(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
// Run analysis with a filter flag ("Pod") to trigger debug output.
|
||||
output := util.CaptureOutput(func() {
|
||||
_ = analysis_RunAnalysisFilterTester(t, "Pod")
|
||||
})
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Filter flags [Pod] specified, run selected core analyzers.",
|
||||
"Debug: PodAnalyzer launched.",
|
||||
"Debug: PodAnalyzer completed without errors.",
|
||||
}
|
||||
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunAnalysis with active filter
|
||||
func TestVerbose_RunAnalysisWithActiveFilter(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
viper.SetDefault("active_filters", "Ingress")
|
||||
output := util.CaptureOutput(func() {
|
||||
_ = analysis_RunAnalysisFilterTester(t, "")
|
||||
})
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Found active filters [Ingress], run selected core analyzers.",
|
||||
"Debug: IngressAnalyzer launched.",
|
||||
"Debug: IngressAnalyzer completed without errors.",
|
||||
}
|
||||
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunAnalysis without any filter (run all core analyzers)
|
||||
func TestVerbose_RunAnalysisWithoutFilter(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
// Clear filter flag and active_filters to run all core analyzers.
|
||||
viper.SetDefault("active_filters", []string{})
|
||||
output := util.CaptureOutput(func() {
|
||||
_ = analysis_RunAnalysisFilterTester(t, "")
|
||||
})
|
||||
|
||||
// Check for debug message indicating no filters.
|
||||
expectedNoFilter := "Debug: No filters selected and no active filters found, run all core analyzers."
|
||||
if !util.Contains(output, expectedNoFilter) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedNoFilter, output)
|
||||
}
|
||||
|
||||
// Get all core analyzers from analyzer.GetAnalyzerMap()
|
||||
coreAnalyzerMap, _ := analyzer.GetAnalyzerMap()
|
||||
for _, analyzerInstance := range coreAnalyzerMap {
|
||||
analyzerType := getTypeName(analyzerInstance)
|
||||
expectedLaunched := fmt.Sprintf("Debug: %s launched.", analyzerType)
|
||||
expectedCompleted := fmt.Sprintf("Debug: %s completed without errors.", analyzerType)
|
||||
if !util.Contains(output, expectedLaunched) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedLaunched, output)
|
||||
}
|
||||
if !util.Contains(output, expectedCompleted) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expectedCompleted, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunCustomAnalysis without custom analyzer
|
||||
func TestVerbose_RunCustomAnalysisWithoutCustomAnalyzer(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
// Set custom_analyzers to empty array to trigger "No custom analyzers" debug message.
|
||||
viper.Set("custom_analyzers", []interface{}{})
|
||||
analysisObj := &Analysis{
|
||||
MaxConcurrency: 1,
|
||||
}
|
||||
output := util.CaptureOutput(func() {
|
||||
analysisObj.RunCustomAnalysis()
|
||||
})
|
||||
expected := "Debug: No custom analyzers found."
|
||||
if !util.Contains(output, "Debug: No custom analyzers found.") {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in RunCustomAnalysis with custom analyzer
|
||||
func TestVerbose_RunCustomAnalysisWithCustomAnalyzer(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
// Set custom_analyzers with one custom analyzer using "fake" connection.
|
||||
viper.Set("custom_analyzers", []map[string]interface{}{
|
||||
{
|
||||
"name": "TestCustomAnalyzer",
|
||||
"connection": map[string]interface{}{"url": "127.0.0.1", "port": "2333"},
|
||||
},
|
||||
})
|
||||
|
||||
analysisObj := &Analysis{
|
||||
MaxConcurrency: 1,
|
||||
}
|
||||
output := util.CaptureOutput(func() {
|
||||
analysisObj.RunCustomAnalysis()
|
||||
})
|
||||
assert.Equal(t, 1, len(analysisObj.Errors)) // connection error
|
||||
|
||||
expectedOutputs := []string{
|
||||
"Debug: Found custom analyzers [TestCustomAnalyzer].",
|
||||
"Debug: TestCustomAnalyzer launched.",
|
||||
"Debug: TestCustomAnalyzer completed with errors.",
|
||||
}
|
||||
|
||||
for _, expected := range expectedOutputs {
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Verbose output in GetAIResults
|
||||
func TestVerbose_GetAIResults(t *testing.T) {
|
||||
viper.Set("verbose", true)
|
||||
disabledCache := cache.New("disabled-cache")
|
||||
disabledCache.DisableCache()
|
||||
aiClient := &ai.NoOpAIClient{}
|
||||
analysisObj := Analysis{
|
||||
AIClient: aiClient,
|
||||
Cache: disabledCache,
|
||||
Results: []common.Result{
|
||||
{
|
||||
Kind: "Deployment",
|
||||
Name: "test-deployment",
|
||||
Error: []common.Failure{{Text: "test-problem", Sensitive: []common.Sensitive{}}},
|
||||
Details: "test-solution",
|
||||
ParentObject: "parent-resource",
|
||||
},
|
||||
},
|
||||
Namespace: "default",
|
||||
}
|
||||
output := util.CaptureOutput(func() {
|
||||
_ = analysisObj.GetAIResults("json", false)
|
||||
})
|
||||
|
||||
expected := "Debug: Generating AI analysis."
|
||||
if !util.Contains(output, expected) {
|
||||
t.Errorf("Expected output to contain: '%s', but got output: '%s'", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,179 +22,283 @@ import (
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
|
||||
"github.com/stretchr/testify/require"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestCronJobAnalyzer(t *testing.T) {
|
||||
suspend := new(bool)
|
||||
*suspend = true
|
||||
|
||||
invalidStartingDeadline := new(int64)
|
||||
*invalidStartingDeadline = -7
|
||||
|
||||
validStartingDeadline := new(int64)
|
||||
*validStartingDeadline = 7
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ1",
|
||||
// This CronJob won't be list because of namespace filtering.
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ2",
|
||||
Namespace: "default",
|
||||
},
|
||||
// A suspended CronJob will contribute to failures.
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Suspend: suspend,
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ3",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
// Valid schedule
|
||||
Schedule: "*/1 * * * *",
|
||||
|
||||
// Negative starting deadline
|
||||
StartingDeadlineSeconds: invalidStartingDeadline,
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ4",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
// Invalid schedule
|
||||
Schedule: "*** * * * *",
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ5",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
// Valid schedule
|
||||
Schedule: "*/1 * * * *",
|
||||
|
||||
// Positive starting deadline shouldn't be any problem.
|
||||
StartingDeadlineSeconds: validStartingDeadline,
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
// This cronjob shouldn't contribute to any failures.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "successful-cronjob",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"analysisDate": "2022-04-01",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/1 * * * *",
|
||||
ConcurrencyPolicy: "Allow",
|
||||
JobTemplate: batchv1.JobTemplateSpec{
|
||||
tests := []struct {
|
||||
name string
|
||||
config common.Analyzer
|
||||
expectations []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "Suspended CronJob",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": "example-app",
|
||||
},
|
||||
Name: "suspended-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "example-container",
|
||||
Image: "nginx",
|
||||
},
|
||||
},
|
||||
RestartPolicy: v1.RestartPolicyOnFailure,
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *",
|
||||
Suspend: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/suspended-job",
|
||||
failuresCount: 1, // One failure for being suspended
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid schedule format",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "invalid-schedule",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "invalid-cron", // Invalid cron format
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/invalid-schedule",
|
||||
failuresCount: 1, // One failure for invalid schedule
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Negative starting deadline",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "negative-deadline",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *",
|
||||
StartingDeadlineSeconds: int64Ptr(-60), // Negative deadline
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/negative-deadline",
|
||||
failuresCount: 1, // One failure for negative deadline
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid CronJob",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "valid-job",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "*/5 * * * *", // Valid cron format
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
// No expectations for valid job
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple issues",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "multiple-issues",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "invalid-cron",
|
||||
StartingDeadlineSeconds: int64Ptr(-60),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/multiple-issues",
|
||||
failuresCount: 2, // Two failures: invalid schedule and negative deadline
|
||||
},
|
||||
},
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
cjAnalyzer := CronJobAnalyzer{}
|
||||
results, err := cjAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
analyzer := CronJobAnalyzer{}
|
||||
results, err := analyzer.Analyze(tt.config)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, len(tt.expectations))
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
// Sort results by name for consistent comparison
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []string{
|
||||
"default/CJ2",
|
||||
"default/CJ3",
|
||||
"default/CJ4",
|
||||
}
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i], result.Name)
|
||||
for i, expectation := range tt.expectations {
|
||||
require.Equal(t, expectation.name, results[i].Name)
|
||||
require.Len(t, results[i].Error, expectation.failuresCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCronJobAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
suspend := new(bool)
|
||||
*suspend = true
|
||||
|
||||
invalidStartingDeadline := new(int64)
|
||||
*invalidStartingDeadline = -7
|
||||
|
||||
validStartingDeadline := new(int64)
|
||||
*validStartingDeadline = 7
|
||||
func TestCronJobAnalyzerLabelSelector(t *testing.T) {
|
||||
clientSet := fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "job-with-label",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "invalid-cron", // This should trigger a failure
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "job-without-label",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: batchv1.CronJobSpec{
|
||||
Schedule: "invalid-cron", // This should trigger a failure
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Test with label selector
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "cronjob",
|
||||
},
|
||||
},
|
||||
},
|
||||
&batchv1.CronJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "CJ2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
),
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=cronjob",
|
||||
LabelSelector: "app=test",
|
||||
}
|
||||
|
||||
cjAnalyzer := CronJobAnalyzer{}
|
||||
results, err := cjAnalyzer.Analyze(config)
|
||||
analyzer := CronJobAnalyzer{}
|
||||
results, err := analyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/CJ1", results[0].Name)
|
||||
require.Equal(t, "default/job-with-label", results[0].Name)
|
||||
}
|
||||
|
||||
func TestCheckCronScheduleIsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedule string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid schedule - every 5 minutes",
|
||||
schedule: "*/5 * * * *",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid schedule - specific time",
|
||||
schedule: "0 2 * * *",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid schedule - complex",
|
||||
schedule: "0 0 1,15 * 3",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid schedule - wrong format",
|
||||
schedule: "invalid-cron",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid schedule - too many fields",
|
||||
schedule: "* * * * * *",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid schedule - empty string",
|
||||
schedule: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := CheckCronScheduleIsValid(tt.schedule)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func int64Ptr(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
|
||||
@@ -28,213 +28,334 @@ import (
|
||||
)
|
||||
|
||||
func TestIngressAnalyzer(t *testing.T) {
|
||||
validIgClassName := new(string)
|
||||
*validIgClassName = "valid-ingress-class"
|
||||
|
||||
var igRule networkingv1.IngressRule
|
||||
|
||||
httpRule := networkingv1.HTTPIngressRuleValue{
|
||||
Paths: []networkingv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/",
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
// This service exists.
|
||||
Name: "Service1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/test1",
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
// This service is in the test namespace
|
||||
// Hence, it won't be discovered.
|
||||
Name: "Service2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/test2",
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
// This service doesn't exist.
|
||||
Name: "Service3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
igRule.IngressRuleValue.HTTP = &httpRule
|
||||
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
// Doesn't specify an ingress class.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress2",
|
||||
Namespace: "default",
|
||||
// Specify an invalid ingress class name using annotations.
|
||||
Annotations: map[string]string{
|
||||
"kubernetes.io/ingress.class": "invalid-class",
|
||||
},
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
// Namespace filtering.
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress3",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&networkingv1.IngressClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: *validIgClassName,
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress4",
|
||||
Namespace: "default",
|
||||
// Specify valid ingress class name using annotations.
|
||||
Annotations: map[string]string{
|
||||
"kubernetes.io/ingress.class": *validIgClassName,
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// Namespace filtering.
|
||||
Name: "Service2",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Secret1",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
&v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Secret2",
|
||||
Namespace: "test",
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress5",
|
||||
Namespace: "default",
|
||||
},
|
||||
|
||||
// Specify valid ingress class name in spec.
|
||||
Spec: networkingv1.IngressSpec{
|
||||
IngressClassName: validIgClassName,
|
||||
Rules: []networkingv1.IngressRule{
|
||||
igRule,
|
||||
},
|
||||
TLS: []networkingv1.IngressTLS{
|
||||
{
|
||||
// This won't contribute to any failures.
|
||||
SecretName: "Secret1",
|
||||
},
|
||||
{
|
||||
// This secret won't be discovered because of namespace filtering.
|
||||
SecretName: "Secret2",
|
||||
},
|
||||
{
|
||||
// This secret doesn't exist.
|
||||
SecretName: "Secret3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
igAnalyzer := IngressAnalyzer{}
|
||||
results, err := igAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
tests := []struct {
|
||||
name string
|
||||
config common.Analyzer
|
||||
expectations []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "default/Ingress1",
|
||||
failuresCount: 1,
|
||||
name: "Missing ingress class",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "no-class",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
// No ingress class specified
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/no-class",
|
||||
failuresCount: 1, // One failure for missing ingress class
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default/Ingress2",
|
||||
failuresCount: 1,
|
||||
name: "Non-existent ingress class",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bad-class",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
IngressClassName: strPtr("non-existent"),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/bad-class",
|
||||
failuresCount: 1, // One failure for non-existent ingress class
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default/Ingress5",
|
||||
failuresCount: 4,
|
||||
name: "Non-existent backend service",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bad-backend",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"kubernetes.io/ingress.class": "nginx",
|
||||
},
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
Rules: []networkingv1.IngressRule{
|
||||
{
|
||||
Host: "example.com",
|
||||
IngressRuleValue: networkingv1.IngressRuleValue{
|
||||
HTTP: &networkingv1.HTTPIngressRuleValue{
|
||||
Paths: []networkingv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/",
|
||||
PathType: pathTypePtr(networkingv1.PathTypePrefix),
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
Name: "non-existent-service",
|
||||
Port: networkingv1.ServiceBackendPort{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/bad-backend",
|
||||
failuresCount: 2, // Two failures: non-existent ingress class and non-existent service
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Non-existent TLS secret",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bad-tls",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"kubernetes.io/ingress.class": "nginx",
|
||||
},
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
TLS: []networkingv1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"example.com"},
|
||||
SecretName: "non-existent-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/bad-tls",
|
||||
failuresCount: 2, // Two failures: non-existent ingress class and non-existent TLS secret
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid ingress with all components",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "valid-ingress",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
IngressClassName: strPtr("nginx"),
|
||||
},
|
||||
},
|
||||
&networkingv1.IngressClass{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "nginx",
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "backend-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
&v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tls-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Type: v1.SecretTypeTLS,
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
// No expectations for valid ingress
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple issues",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "multiple-issues",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
IngressClassName: strPtr("non-existent"),
|
||||
Rules: []networkingv1.IngressRule{
|
||||
{
|
||||
Host: "example.com",
|
||||
IngressRuleValue: networkingv1.IngressRuleValue{
|
||||
HTTP: &networkingv1.HTTPIngressRuleValue{
|
||||
Paths: []networkingv1.HTTPIngressPath{
|
||||
{
|
||||
Path: "/",
|
||||
PathType: pathTypePtr(networkingv1.PathTypePrefix),
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
Name: "non-existent-service",
|
||||
Port: networkingv1.ServiceBackendPort{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: []networkingv1.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"example.com"},
|
||||
SecretName: "non-existent-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/multiple-issues",
|
||||
failuresCount: 3, // Three failures: ingress class, service, and TLS secret
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
analyzer := IngressAnalyzer{}
|
||||
results, err := analyzer.Analyze(tt.config)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, len(tt.expectations))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
// Sort results by name for consistent comparison
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
for i, expectation := range tt.expectations {
|
||||
require.Equal(t, expectation.name, results[i].Name)
|
||||
require.Len(t, results[i].Error, expectation.failuresCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressAnalyzerLabelSelectorFiltering(t *testing.T) {
|
||||
validIgClassName := new(string)
|
||||
*validIgClassName = "valid-ingress-class"
|
||||
func TestIngressAnalyzerLabelSelector(t *testing.T) {
|
||||
clientSet := fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ingress-with-label",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
// Missing ingress class to trigger a failure
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ingress-without-label",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
// Missing ingress class to trigger a failure
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Test with label selector
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "ingress",
|
||||
},
|
||||
},
|
||||
},
|
||||
&networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Ingress2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
),
|
||||
Client: clientSet,
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "default",
|
||||
LabelSelector: "app=ingress",
|
||||
LabelSelector: "app=test",
|
||||
}
|
||||
|
||||
igAnalyzer := IngressAnalyzer{}
|
||||
results, err := igAnalyzer.Analyze(config)
|
||||
analyzer := IngressAnalyzer{}
|
||||
results, err := analyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(results))
|
||||
require.Equal(t, "default/Ingress1", results[0].Name)
|
||||
|
||||
require.Equal(t, "default/ingress-with-label", results[0].Name)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func pathTypePtr(p networkingv1.PathType) *networkingv1.PathType {
|
||||
return &p
|
||||
}
|
||||
|
||||
@@ -24,145 +24,232 @@ import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
)
|
||||
|
||||
func TestServiceAnalyzer(t *testing.T) {
|
||||
config := common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Endpoint1",
|
||||
Namespace: "test",
|
||||
},
|
||||
// Endpoint with non-zero subsets.
|
||||
Subsets: []v1.EndpointSubset{
|
||||
{
|
||||
// These not ready end points will contribute to failures.
|
||||
NotReadyAddresses: []v1.EndpointAddress{
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "test-reference",
|
||||
Name: "reference1",
|
||||
},
|
||||
},
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "test-reference",
|
||||
Name: "reference2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// These not ready end points will contribute to failures.
|
||||
NotReadyAddresses: []v1.EndpointAddress{
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "test-reference",
|
||||
Name: "reference3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Endpoint2",
|
||||
Namespace: "test",
|
||||
Annotations: map[string]string{
|
||||
// Leader election record annotation key defined.
|
||||
resourcelock.LeaderElectionRecordAnnotationKey: "this is okay",
|
||||
},
|
||||
},
|
||||
// Endpoint with zero subsets.
|
||||
},
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// This won't contribute to any failures.
|
||||
Name: "non-existent-service",
|
||||
Namespace: "test",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
// Endpoint with zero subsets.
|
||||
},
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service1",
|
||||
Namespace: "test",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
// Endpoint with zero subsets.
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service1",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app1": "test-app1",
|
||||
"app2": "test-app2",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// This service won't be discovered.
|
||||
Name: "Service2",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app1": "test-app1",
|
||||
"app2": "test-app2",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "Service3",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
// No Spec Selector
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Context: context.Background(),
|
||||
Namespace: "test",
|
||||
}
|
||||
|
||||
sAnalyzer := ServiceAnalyzer{}
|
||||
results, err := sAnalyzer.Analyze(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
expectations := []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
tests := []struct {
|
||||
name string
|
||||
config common.Analyzer
|
||||
expectations []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "test/Endpoint1",
|
||||
failuresCount: 1,
|
||||
name: "Service with no endpoints",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{}, // Empty subsets
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/test-service",
|
||||
failuresCount: 1, // One failure for no endpoints
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test/Service1",
|
||||
failuresCount: 2,
|
||||
name: "Service with not ready endpoints",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{
|
||||
{
|
||||
NotReadyAddresses: []v1.EndpointAddress{
|
||||
{
|
||||
TargetRef: &v1.ObjectReference{
|
||||
Kind: "Pod",
|
||||
Name: "test-pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/test-service",
|
||||
failuresCount: 1, // One failure for not ready endpoints
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Service with warning events",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{}, // Empty subsets
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
&v1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-event",
|
||||
Namespace: "default",
|
||||
},
|
||||
InvolvedObject: v1.ObjectReference{
|
||||
Kind: "Service",
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Type: "Warning",
|
||||
Reason: "TestReason",
|
||||
Message: "Test warning message",
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
{
|
||||
name: "default/test-service",
|
||||
failuresCount: 2, // One failure for no endpoints, one for warning event
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Service with leader election annotation",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"control-plane.alpha.kubernetes.io/leader": "test-leader",
|
||||
},
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{}, // Empty subsets
|
||||
},
|
||||
&v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
// No expectations for leader election endpoints
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Service with non-existent service",
|
||||
config: common.Analyzer{
|
||||
Client: &kubernetes.Client{
|
||||
Client: fake.NewSimpleClientset(
|
||||
&v1.Endpoints{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service",
|
||||
Namespace: "default",
|
||||
},
|
||||
Subsets: []v1.EndpointSubset{}, // Empty subsets
|
||||
},
|
||||
),
|
||||
},
|
||||
Namespace: "default",
|
||||
},
|
||||
expectations: []struct {
|
||||
name string
|
||||
failuresCount int
|
||||
}{
|
||||
// No expectations for non-existent service
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
require.Equal(t, len(expectations), len(results))
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
analyzer := ServiceAnalyzer{}
|
||||
results, err := analyzer.Analyze(tt.config)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, len(tt.expectations))
|
||||
|
||||
for i, result := range results {
|
||||
require.Equal(t, expectations[i].name, result.Name)
|
||||
require.Equal(t, expectations[i].failuresCount, len(result.Error))
|
||||
// Sort results by name for consistent comparison
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Name < results[j].Name
|
||||
})
|
||||
|
||||
for i, expectation := range tt.expectations {
|
||||
require.Equal(t, expectation.name, results[i].Name)
|
||||
require.Len(t, results[i].Error, expectation.failuresCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
)
|
||||
|
||||
@@ -10,9 +12,50 @@ func (h *Handler) Query(ctx context.Context, i *schemav1.QueryRequest) (
|
||||
*schemav1.QueryResponse,
|
||||
error,
|
||||
) {
|
||||
aiClient := ai.NewClient(i.Backend)
|
||||
// Create client factory and config provider
|
||||
factory := ai.GetAIClientFactory()
|
||||
configProvider := ai.GetConfigProvider()
|
||||
|
||||
// Use the factory to create the client
|
||||
aiClient := factory.NewClient(i.Backend)
|
||||
defer aiClient.Close()
|
||||
|
||||
var configAI ai.AIConfiguration
|
||||
if err := configProvider.UnmarshalKey("ai", &configAI); err != nil {
|
||||
return &schemav1.QueryResponse{
|
||||
Response: "",
|
||||
Error: &schemav1.QueryError{
|
||||
Message: fmt.Sprintf("Failed to unmarshal AI configuration: %v", err),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var aiProvider ai.AIProvider
|
||||
for _, provider := range configAI.Providers {
|
||||
if i.Backend == provider.Name {
|
||||
aiProvider = provider
|
||||
break
|
||||
}
|
||||
}
|
||||
if aiProvider.Name == "" {
|
||||
return &schemav1.QueryResponse{
|
||||
Response: "",
|
||||
Error: &schemav1.QueryError{
|
||||
Message: fmt.Sprintf("AI provider %s not found in configuration", i.Backend),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Configure the AI client
|
||||
if err := aiClient.Configure(&aiProvider); err != nil {
|
||||
return &schemav1.QueryResponse{
|
||||
Response: "",
|
||||
Error: &schemav1.QueryError{
|
||||
Message: fmt.Sprintf("Failed to configure AI client: %v", err),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
resp, err := aiClient.GetCompletion(ctx, i.Query)
|
||||
var errMessage string = ""
|
||||
if err != nil {
|
||||
|
||||
310
pkg/server/query/query_test.go
Normal file
310
pkg/server/query/query_test.go
Normal file
@@ -0,0 +1,310 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
schemav1 "buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go/schema/v1"
|
||||
"github.com/k8sgpt-ai/k8sgpt/pkg/ai"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockAI is a mock implementation of the ai.IAI interface for testing
|
||||
type MockAI struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockAI) Configure(config ai.IAIConfig) error {
|
||||
args := m.Called(config)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockAI) GetCompletion(ctx context.Context, prompt string) (string, error) {
|
||||
args := m.Called(ctx, prompt)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockAI) GetName() string {
|
||||
args := m.Called()
|
||||
return args.String(0)
|
||||
}
|
||||
|
||||
func (m *MockAI) Close() {
|
||||
m.Called()
|
||||
}
|
||||
|
||||
// MockAIClientFactory is a mock implementation of AIClientFactory
|
||||
type MockAIClientFactory struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockAIClientFactory) NewClient(provider string) ai.IAI {
|
||||
args := m.Called(provider)
|
||||
return args.Get(0).(ai.IAI)
|
||||
}
|
||||
|
||||
// MockConfigProvider is a mock implementation of ConfigProvider
|
||||
type MockConfigProvider struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockConfigProvider) UnmarshalKey(key string, rawVal interface{}) error {
|
||||
args := m.Called(key, rawVal)
|
||||
|
||||
// If we want to set the rawVal (which is a pointer)
|
||||
if fn, ok := args.Get(0).(func(interface{})); ok && fn != nil {
|
||||
fn(rawVal)
|
||||
}
|
||||
|
||||
// Return the error as the first return value
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestQuery_Success(t *testing.T) {
|
||||
// Setup mocks
|
||||
mockAI := new(MockAI)
|
||||
mockFactory := new(MockAIClientFactory)
|
||||
mockConfig := new(MockConfigProvider)
|
||||
|
||||
// Set test implementations
|
||||
ai.SetTestAIClientFactory(mockFactory)
|
||||
ai.SetTestConfigProvider(mockConfig)
|
||||
defer ai.ResetTestImplementations()
|
||||
|
||||
// Define test data
|
||||
testBackend := "test-backend"
|
||||
testQuery := "test query"
|
||||
testResponse := "test response"
|
||||
|
||||
// Setup expectations
|
||||
mockFactory.On("NewClient", testBackend).Return(mockAI)
|
||||
mockAI.On("Close").Return()
|
||||
|
||||
// Set up configuration with a valid provider
|
||||
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) {
|
||||
config := args.Get(1).(*ai.AIConfiguration)
|
||||
*config = ai.AIConfiguration{
|
||||
Providers: []ai.AIProvider{
|
||||
{
|
||||
Name: testBackend,
|
||||
Password: "test-password",
|
||||
Model: "test-model",
|
||||
},
|
||||
},
|
||||
}
|
||||
}).Return(nil)
|
||||
|
||||
mockAI.On("Configure", mock.AnythingOfType("*ai.AIProvider")).Return(nil)
|
||||
mockAI.On("GetCompletion", mock.Anything, testQuery).Return(testResponse, nil)
|
||||
|
||||
// Create handler and call Query
|
||||
handler := &Handler{}
|
||||
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
|
||||
Backend: testBackend,
|
||||
Query: testQuery,
|
||||
})
|
||||
|
||||
// Assertions
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.Equal(t, testResponse, response.Response)
|
||||
assert.Equal(t, "", response.Error.Message)
|
||||
|
||||
// Verify mocks
|
||||
mockAI.AssertExpectations(t)
|
||||
mockFactory.AssertExpectations(t)
|
||||
mockConfig.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestQuery_UnmarshalError(t *testing.T) {
|
||||
// Setup mocks
|
||||
mockAI := new(MockAI)
|
||||
mockFactory := new(MockAIClientFactory)
|
||||
mockConfig := new(MockConfigProvider)
|
||||
|
||||
// Set test implementations
|
||||
ai.SetTestAIClientFactory(mockFactory)
|
||||
ai.SetTestConfigProvider(mockConfig)
|
||||
defer ai.ResetTestImplementations()
|
||||
|
||||
// Setup expectations
|
||||
mockFactory.On("NewClient", "test-backend").Return(mockAI)
|
||||
mockAI.On("Close").Return()
|
||||
|
||||
// Mock unmarshal error
|
||||
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Return(errors.New("unmarshal error"))
|
||||
|
||||
// Create handler and call Query
|
||||
handler := &Handler{}
|
||||
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
|
||||
Backend: "test-backend",
|
||||
Query: "test query",
|
||||
})
|
||||
|
||||
// Assertions
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.Equal(t, "", response.Response)
|
||||
assert.Contains(t, response.Error.Message, "Failed to unmarshal AI configuration")
|
||||
|
||||
// Verify mocks
|
||||
mockAI.AssertExpectations(t)
|
||||
mockFactory.AssertExpectations(t)
|
||||
mockConfig.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestQuery_ProviderNotFound(t *testing.T) {
|
||||
// Setup mocks
|
||||
mockAI := new(MockAI)
|
||||
mockFactory := new(MockAIClientFactory)
|
||||
mockConfig := new(MockConfigProvider)
|
||||
|
||||
// Set test implementations
|
||||
ai.SetTestAIClientFactory(mockFactory)
|
||||
ai.SetTestConfigProvider(mockConfig)
|
||||
defer ai.ResetTestImplementations()
|
||||
|
||||
// Define test data
|
||||
testBackend := "test-backend"
|
||||
|
||||
// Setup expectations
|
||||
mockFactory.On("NewClient", testBackend).Return(mockAI)
|
||||
mockAI.On("Close").Return()
|
||||
|
||||
// Set up configuration with no matching provider
|
||||
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) {
|
||||
config := args.Get(1).(*ai.AIConfiguration)
|
||||
*config = ai.AIConfiguration{
|
||||
Providers: []ai.AIProvider{
|
||||
{
|
||||
Name: "other-backend",
|
||||
},
|
||||
},
|
||||
}
|
||||
}).Return(nil)
|
||||
|
||||
// Create handler and call Query
|
||||
handler := &Handler{}
|
||||
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
|
||||
Backend: testBackend,
|
||||
Query: "test query",
|
||||
})
|
||||
|
||||
// Assertions
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.Equal(t, "", response.Response)
|
||||
assert.Contains(t, response.Error.Message, "AI provider test-backend not found in configuration")
|
||||
|
||||
// Verify mocks
|
||||
mockAI.AssertExpectations(t)
|
||||
mockFactory.AssertExpectations(t)
|
||||
mockConfig.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestQuery_ConfigureError(t *testing.T) {
|
||||
// Setup mocks
|
||||
mockAI := new(MockAI)
|
||||
mockFactory := new(MockAIClientFactory)
|
||||
mockConfig := new(MockConfigProvider)
|
||||
|
||||
// Set test implementations
|
||||
ai.SetTestAIClientFactory(mockFactory)
|
||||
ai.SetTestConfigProvider(mockConfig)
|
||||
defer ai.ResetTestImplementations()
|
||||
|
||||
// Define test data
|
||||
testBackend := "test-backend"
|
||||
|
||||
// Setup expectations
|
||||
mockFactory.On("NewClient", testBackend).Return(mockAI)
|
||||
mockAI.On("Close").Return()
|
||||
|
||||
// Set up configuration with a valid provider
|
||||
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) {
|
||||
config := args.Get(1).(*ai.AIConfiguration)
|
||||
*config = ai.AIConfiguration{
|
||||
Providers: []ai.AIProvider{
|
||||
{
|
||||
Name: testBackend,
|
||||
},
|
||||
},
|
||||
}
|
||||
}).Return(nil)
|
||||
|
||||
// Mock configure error
|
||||
mockAI.On("Configure", mock.AnythingOfType("*ai.AIProvider")).Return(errors.New("configure error"))
|
||||
|
||||
// Create handler and call Query
|
||||
handler := &Handler{}
|
||||
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
|
||||
Backend: testBackend,
|
||||
Query: "test query",
|
||||
})
|
||||
|
||||
// Assertions
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.Equal(t, "", response.Response)
|
||||
assert.Contains(t, response.Error.Message, "Failed to configure AI client")
|
||||
|
||||
// Verify mocks
|
||||
mockAI.AssertExpectations(t)
|
||||
mockFactory.AssertExpectations(t)
|
||||
mockConfig.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestQuery_GetCompletionError(t *testing.T) {
|
||||
// Setup mocks
|
||||
mockAI := new(MockAI)
|
||||
mockFactory := new(MockAIClientFactory)
|
||||
mockConfig := new(MockConfigProvider)
|
||||
|
||||
// Set test implementations
|
||||
ai.SetTestAIClientFactory(mockFactory)
|
||||
ai.SetTestConfigProvider(mockConfig)
|
||||
defer ai.ResetTestImplementations()
|
||||
|
||||
// Define test data
|
||||
testBackend := "test-backend"
|
||||
testQuery := "test query"
|
||||
|
||||
// Setup expectations
|
||||
mockFactory.On("NewClient", testBackend).Return(mockAI)
|
||||
mockAI.On("Close").Return()
|
||||
|
||||
// Set up configuration with a valid provider
|
||||
mockConfig.On("UnmarshalKey", "ai", mock.Anything).Run(func(args mock.Arguments) {
|
||||
config := args.Get(1).(*ai.AIConfiguration)
|
||||
*config = ai.AIConfiguration{
|
||||
Providers: []ai.AIProvider{
|
||||
{
|
||||
Name: testBackend,
|
||||
},
|
||||
},
|
||||
}
|
||||
}).Return(nil)
|
||||
|
||||
mockAI.On("Configure", mock.AnythingOfType("*ai.AIProvider")).Return(nil)
|
||||
mockAI.On("GetCompletion", mock.Anything, testQuery).Return("", errors.New("completion error"))
|
||||
|
||||
// Create handler and call Query
|
||||
handler := &Handler{}
|
||||
response, err := handler.Query(context.Background(), &schemav1.QueryRequest{
|
||||
Backend: testBackend,
|
||||
Query: testQuery,
|
||||
})
|
||||
|
||||
// Assertions
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, response)
|
||||
assert.Equal(t, "", response.Response)
|
||||
assert.Equal(t, "completion error", response.Error.Message)
|
||||
|
||||
// Verify mocks
|
||||
mockAI.AssertExpectations(t)
|
||||
mockFactory.AssertExpectations(t)
|
||||
mockConfig.AssertExpectations(t)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ limitations under the License.
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
@@ -311,3 +312,33 @@ func LabelStrToSelector(labelStr string) labels.Selector {
|
||||
}
|
||||
return labels.SelectorFromSet(labels.Set(labelSelectorMap))
|
||||
}
|
||||
|
||||
// CaptureOutput captures the output of a function that writes to stdout
|
||||
func CaptureOutput(f func()) string {
|
||||
old := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create pipe: %v", err))
|
||||
}
|
||||
os.Stdout = w
|
||||
// Ensure os.Stdout is restored even if panic occurs
|
||||
defer func() {
|
||||
os.Stdout = old
|
||||
}()
|
||||
|
||||
f()
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
panic(fmt.Sprintf("failed to close writer: %v", err))
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if _, err := buf.ReadFrom(r); err != nil {
|
||||
panic(fmt.Sprintf("failed to read from pipe: %v", err))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Contains checks if substr is present in s
|
||||
func Contains(s, substr string) bool {
|
||||
return bytes.Contains([]byte(s), []byte(substr))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user