diff --git a/README.md b/README.md index 58a715afc8..86bd9d736e 100644 --- a/README.md +++ b/README.md @@ -502,6 +502,21 @@ This now gives the ability to pass through hostOS information ( from this analyz _See the docs on how to write a custom analyzer_ +_Listing custom analyzers configured_ +``` +k8sgpt custom-analyzer list +``` + +_Adding custom analyzer without install_ +``` +k8sgpt custom-analyzer add --name my-custom-analyzer --port 8085 +``` + +_Removing custom analyzer_ +``` +k8sgpt custom-analyzer remove --names "my-custom-analyzer,my-custom-analyzer-2" +``` + ## Documentation diff --git a/cmd/customAnalyzer/add.go b/cmd/customAnalyzer/add.go new file mode 100644 index 0000000000..91dbdb9896 --- /dev/null +++ b/cmd/customAnalyzer/add.go @@ -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 customanalyzer + +import ( + "os" + + "github.com/fatih/color" + customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + name string + url string + port int +) + +var addCmd = &cobra.Command{ + Use: "add", + Aliases: []string{"add"}, + Short: "This command will add a custom analyzer from source", + Long: "This command allows you to add/remote/list an existing custom analyzer.", + Run: func(cmd *cobra.Command, args []string) { + err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer) + if err != nil { + color.Red("Error: %v", err) + os.Exit(1) + } + analyzer := customAnalyzer.NewCustomAnalyzer() + + // Check if configuration is valid + err = analyzer.Check(configCustomAnalyzer, name, url, port) + if err != nil { + color.Red("Error adding custom analyzer: %s", err.Error()) + os.Exit(1) + } + + configCustomAnalyzer = append(configCustomAnalyzer, customAnalyzer.CustomAnalyzerConfiguration{ + Name: name, + Connection: customAnalyzer.Connection{ + Url: url, + Port: port, + }, + }) + + viper.Set("custom_analyzers", configCustomAnalyzer) + if err := viper.WriteConfig(); err != nil { + color.Red("Error writing config file: %s", err.Error()) + os.Exit(1) + } + color.Green("%s added to the custom analyzers config list", name) + + }, +} + +func init() { + addCmd.Flags().StringVarP(&name, "name", "n", "my-custom-analyzer", "Name of the custom analyzer.") + addCmd.Flags().StringVarP(&url, "url", "u", "localhost", "URL for the custom analyzer connection.") + addCmd.Flags().IntVarP(&port, "port", "r", 8085, "Port for the custom analyzer connection.") +} diff --git a/cmd/customAnalyzer/customAnalyzer.go b/cmd/customAnalyzer/customAnalyzer.go new file mode 100644 index 0000000000..934ad7d4e2 --- /dev/null +++ b/cmd/customAnalyzer/customAnalyzer.go @@ -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 customanalyzer + +import ( + customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer" + "github.com/spf13/cobra" +) + +var configCustomAnalyzer []customAnalyzer.CustomAnalyzerConfiguration + +// authCmd represents the auth command +var CustomAnalyzerCmd = &cobra.Command{ + Use: "custom-analyzer", + Short: "Manage a custom analyzer", + Long: `This command allows you to manage custom analyzers, including adding, removing, and listing them.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + _ = cmd.Help() + return + } + }, +} + +func init() { + // add subcommand to add custom analyzer + CustomAnalyzerCmd.AddCommand(addCmd) + // remove subcomment to remove custom analyzer + CustomAnalyzerCmd.AddCommand(removeCmd) + // list subcomment to list custom analyzer + CustomAnalyzerCmd.AddCommand(listCmd) +} diff --git a/cmd/customAnalyzer/list.go b/cmd/customAnalyzer/list.go new file mode 100644 index 0000000000..d7027aa362 --- /dev/null +++ b/cmd/customAnalyzer/list.go @@ -0,0 +1,60 @@ +/* +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 customanalyzer + +import ( + "fmt" + "os" + + "github.com/fatih/color" + customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var details bool + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List configured custom analyzers", + Long: "The list command displays a list of configured custom analyzers", + Run: func(cmd *cobra.Command, args []string) { + + // get custom_analyzers configuration + err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer) + if err != nil { + color.Red("Error: %v", err) + os.Exit(1) + } + + // Get list of all Custom Analyers configured + fmt.Print(color.YellowString("Active: \n")) + for _, analyzer := range configCustomAnalyzer { + fmt.Printf("> %s\n", color.GreenString(analyzer.Name)) + if details { + printDetails(analyzer) + } + } + }, +} + +func init() { + listCmd.Flags().BoolVar(&details, "details", false, "Print custom analyzers configuration details") +} + +func printDetails(analyzer customAnalyzer.CustomAnalyzerConfiguration) { + fmt.Printf(" - Url: %s\n", analyzer.Connection.Url) + fmt.Printf(" - Port: %d\n", analyzer.Connection.Port) + +} diff --git a/cmd/customAnalyzer/remove.go b/cmd/customAnalyzer/remove.go new file mode 100644 index 0000000000..f9f19471c9 --- /dev/null +++ b/cmd/customAnalyzer/remove.go @@ -0,0 +1,90 @@ +/* +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 customanalyzer + +import ( + "os" + "strings" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + names string +) + +var removeCmd = &cobra.Command{ + Use: "remove", + Short: "Remove custom analyzer(s)", + Long: "The command to remove custom analyzer(s)", + PreRun: func(cmd *cobra.Command, args []string) { + // Ensure that the "names" flag is provided before running the command + _ = cmd.MarkFlagRequired("names") + }, + Run: func(cmd *cobra.Command, args []string) { + if names == "" { + // Display an error message and show command help if "names" is not set + color.Red("Error: names must be set.") + _ = cmd.Help() + return + } + // Split the provided names by comma + inputCustomAnalyzers := strings.Split(names, ",") + + // Load the custom analyzers from the configuration file + err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer) + if err != nil { + // Display an error message if the configuration cannot be loaded + color.Red("Error: %v", err) + os.Exit(1) + } + + // Iterate over each input analyzer name + for _, inputAnalyzer := range inputCustomAnalyzers { + foundAnalyzer := false + // Search for the analyzer in the current configuration + for i, analyzer := range configCustomAnalyzer { + if analyzer.Name == inputAnalyzer { + foundAnalyzer = true + + // Remove the analyzer from the configuration list + configCustomAnalyzer = append(configCustomAnalyzer[:i], configCustomAnalyzer[i+1:]...) + color.Green("%s deleted from the custom analyzer list", analyzer.Name) + break + } + } + if !foundAnalyzer { + // Display an error if the analyzer is not found in the configuration + color.Red("Error: %s does not exist in configuration file. Please use k8sgpt custom-analyzer add.", inputAnalyzer) + os.Exit(1) + } + } + + // Save the updated configuration back to the file + viper.Set("custom_analyzers", configCustomAnalyzer) + if err := viper.WriteConfig(); err != nil { + // Display an error if the configuration cannot be written + color.Red("Error writing config file: %s", err.Error()) + os.Exit(1) + } + + }, +} + +func init() { + // add flag for names + removeCmd.Flags().StringVarP(&names, "names", "n", "", "Custom analyzers to remove (separated by a comma)") +} diff --git a/cmd/root.go b/cmd/root.go index 13f74dd40c..16e1b2c0d8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,6 +22,7 @@ import ( "github.com/k8sgpt-ai/k8sgpt/cmd/analyze" "github.com/k8sgpt-ai/k8sgpt/cmd/auth" "github.com/k8sgpt-ai/k8sgpt/cmd/cache" + customanalyzer "github.com/k8sgpt-ai/k8sgpt/cmd/customAnalyzer" "github.com/k8sgpt-ai/k8sgpt/cmd/filters" "github.com/k8sgpt-ai/k8sgpt/cmd/generate" "github.com/k8sgpt-ai/k8sgpt/cmd/integration" @@ -74,6 +75,7 @@ func init() { rootCmd.AddCommand(integration.IntegrationCmd) rootCmd.AddCommand(serve.ServeCmd) rootCmd.AddCommand(cache.CacheCmd) + rootCmd.AddCommand(customanalyzer.CustomAnalyzerCmd) 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.") diff --git a/go.mod b/go.mod index 74a9039548..2482c17346 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.12.4 // indirect github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect github.com/anchore/go-struct-converter v0.0.0-20230627203149-c72ef8859ca9 // indirect @@ -76,6 +77,7 @@ require ( github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-kit/log v0.2.1 // indirect @@ -94,6 +96,7 @@ require ( github.com/jpillora/backoff v1.0.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect @@ -141,9 +144,9 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v26.1.4+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v27.0.0+incompatible // indirect + github.com/docker/docker v27.0.0+incompatible github.com/docker/docker-credential-helpers v0.8.2 // indirect - github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-connections v0.5.0 github.com/docker/go-metrics v0.0.1 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch v5.9.0+incompatible // indirect diff --git a/go.sum b/go.sum index ea81b062a8..1e6b2cd1a1 100644 --- a/go.sum +++ b/go.sum @@ -1497,6 +1497,9 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= @@ -2097,6 +2100,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -2349,6 +2354,10 @@ go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFu go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= @@ -2372,6 +2381,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.starlark.net v0.0.0-20240520160348-046347dcd104 h1:3qhteRISupnJvaWshOmeqEUs2y9oc/+/ePPvDh3Eygg= go.starlark.net v0.0.0-20240520160348-046347dcd104/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= diff --git a/pkg/custom_analyzer/customAnalyzer.go b/pkg/custom_analyzer/customAnalyzer.go new file mode 100644 index 0000000000..ed3e03acce --- /dev/null +++ b/pkg/custom_analyzer/customAnalyzer.go @@ -0,0 +1,46 @@ +package custom_analyzer + +import ( + "fmt" + "reflect" + "regexp" +) + +type CustomAnalyzerConfiguration struct { + Name string `mapstructure:"name"` + Connection Connection `mapstructure:"connection"` +} + +type Connection struct { + Url string `mapstructure:"url"` + Port int `mapstructure:"port"` +} + +type CustomAnalyzer struct{} + +func NewCustomAnalyzer() *CustomAnalyzer { + return &CustomAnalyzer{} +} + +func (*CustomAnalyzer) Check(actualConfig []CustomAnalyzerConfiguration, name, url string, port int) error { + validNameRegex := `^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` + validName := regexp.MustCompile(validNameRegex) + if !validName.MatchString(name) { + return fmt.Errorf("invalid name format. Must match %s", validNameRegex) + } + + for _, analyzer := range actualConfig { + if analyzer.Name == name { + return fmt.Errorf("custom analyzer with the name '%s' already exists. Please use a different name", name) + } + + if reflect.DeepEqual(analyzer.Connection, Connection{ + Url: url, + Port: port, + }) { + return fmt.Errorf("custom analyzer with the same connection configuration (URL: '%s', Port: %d) already exists. Please use a different URL or port", url, port) + } + } + + return nil +}