From ff633974b6fdefdb0375010940f5ffd7fa641c9e Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Sun, 21 Jul 2024 11:45:36 +0200 Subject: [PATCH 01/10] feat: add custom analyzer management capability Introduced the ability to manage custom analyzers in the K8sGPT application, enabling users to add, deploy, and configure custom analyzers from various sources. This enhancement supports extending the application's analytical capabilities by integrating external analysis tools, thus offering more flexibility and customization options to meet specific user needs. Signed-off-by: Matthis Holleville --- cmd/customAnalyzer/add.go | 102 ++++++++++++++++++++++++++ cmd/customAnalyzer/customAnalyzer.go | 39 ++++++++++ cmd/root.go | 2 + go.mod | 7 +- go.sum | 20 +++--- pkg/customAnalyzer/customAnalyzer.go | 58 +++++++++++++++ pkg/customAnalyzer/docker/docker.go | 103 +++++++++++++++++++++++++++ 7 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 cmd/customAnalyzer/add.go create mode 100644 cmd/customAnalyzer/customAnalyzer.go create mode 100644 pkg/customAnalyzer/customAnalyzer.go create mode 100644 pkg/customAnalyzer/docker/docker.go diff --git a/cmd/customAnalyzer/add.go b/cmd/customAnalyzer/add.go new file mode 100644 index 0000000000..3f42a4dd0c --- /dev/null +++ b/cmd/customAnalyzer/add.go @@ -0,0 +1,102 @@ +/* +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/customAnalyzer" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + installType string + install bool + packageUrl string + 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 a custom analyzer from a specified source and optionally install it.", + PreRun: func(cmd *cobra.Command, args []string) { + if install { + _ = cmd.MarkFlagRequired("install-type") + _ = cmd.MarkFlagRequired("package") + } + }, + Run: func(cmd *cobra.Command, args []string) { + err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer) + if err != nil { + color.Red("Error: %v", err) + os.Exit(1) + } + customAnalyzer := customanalyzer.NewCustomAnalyzer() + + // Check if configuration is valid + err = customAnalyzer.Check(configCustomAnalyzer, name, url, port) + if err != nil { + color.Red("Error adding custom analyzer: %s", err.Error()) + os.Exit(1) + } + + if install { + // Check if installType is possible + install, err := customAnalyzer.GetInstallType(installType) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // create a pod in-cluster with custom analyzer + err = install.Deploy(packageUrl, name, url, port) + if err != nil { + color.Red("Error installing 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(&installType, "install-type", "t", "docker", "Specify the installation type (e.g., docker, kubernetes).") + AddCmd.Flags().BoolVarP(&install, "install", "i", false, "Flag to indicate whether to install the custom analyzer after adding.") + AddCmd.Flags().StringVarP(&packageUrl, "package", "p", "", "URL of the custom analyzer package.") + 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..31679cd9a3 --- /dev/null +++ b/cmd/customAnalyzer/customAnalyzer.go @@ -0,0 +1,39 @@ +/* +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/customAnalyzer" + "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) +} 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 aa28c20151..7f2a40ec91 100644 --- a/go.mod +++ b/go.mod @@ -67,6 +67,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 @@ -75,6 +76,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 @@ -93,6 +95,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 @@ -140,9 +143,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 dc737049ac..cb256fd900 100644 --- a/go.sum +++ b/go.sum @@ -8,18 +8,10 @@ atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.20.0-20240406062209-1cc152efbf5c.1 h1:0x36l6ETxg5YDlfFTxSR+4SpL0bwLezTCUfGdcPUN44= buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.20.0-20240406062209-1cc152efbf5c.1/go.mod h1:/n44w/baTCuEmDgCBgSxQ2GEiO7N645eKxLKbygzW4s= -buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.20.0-20240717144446-c4efcc29ff16.2 h1:68+/5HyHF3WAm5PtNvRwuzSqTD/im9JlylgyneHCPVY= -buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.20.0-20240717144446-c4efcc29ff16.2/go.mod h1:eYww1zlm6K5Tfwo3AfcVNMhnGJXR32t/PbZiPbvzv4s= buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20240406062209-1cc152efbf5c.3 h1:EiautHLlnNmBZdh1wFpmrSDvV4t8sucXGwV6vaE8Xuc= buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20240406062209-1cc152efbf5c.3/go.mod h1:4QVX5iWdNcwSFhpXXIXwVH7qT/g9LKvxiqa0SvYJ9hE= -buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20240717144446-c4efcc29ff16.3 h1:3HaNXtw/bEPWPXSqwLgEtBvqh39HLPYnFzG5UmZ00hs= -buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20240717144446-c4efcc29ff16.3/go.mod h1:2FnWdzuB/BRUpuEQ71s/EviXJvVzzlHv8BvTlFFYgmQ= buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.0-20240406062209-1cc152efbf5c.1/go.mod h1:qFzoT6sNuRF9vPeDFmxd9KZ1YgU2vnnno5E5I0OUjOc= -buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.0-20240717144446-c4efcc29ff16.1/go.mod h1:qFzoT6sNuRF9vPeDFmxd9KZ1YgU2vnnno5E5I0OUjOc= -buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.1-20240406062209-1cc152efbf5c.1 h1:DLKuL/RwZg0bRweSS18Bi67GzKOW3F6YnVU0nZYXZBU= buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.1-20240406062209-1cc152efbf5c.1/go.mod h1:qEarbrHjaZEQ5GeUH6XqSqqJMvtPwAGFpAc0nkSBzrQ= -buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.1-20240717144446-c4efcc29ff16.1 h1:TJj+JSRc64sitUGL1XaCPY/OU090/5lNi4ABLG9jTSQ= -buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.1-20240717144446-c4efcc29ff16.1/go.mod h1:qEarbrHjaZEQ5GeUH6XqSqqJMvtPwAGFpAc0nkSBzrQ= buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.2-20240717144446-c4efcc29ff16.2 h1:INIYy743CJ4MEZu+O4by7oeC/m+a/l4HBk79FshPGBI= buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.2-20240717144446-c4efcc29ff16.2/go.mod h1:1wq1qVxvJkTEUQsF5/XjmhQYXYhbVoLSGhKnzS3ie54= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -1504,6 +1496,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= @@ -2104,6 +2099,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= @@ -2356,6 +2353,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= @@ -2379,6 +2380,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= @@ -3322,7 +3325,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= helm.sh/helm/v3 v3.15.2 h1:/3XINUFinJOBjQplGnjw92eLGpgXXp1L8chWPkCkDuw= diff --git a/pkg/customAnalyzer/customAnalyzer.go b/pkg/customAnalyzer/customAnalyzer.go new file mode 100644 index 0000000000..0b2d1b1137 --- /dev/null +++ b/pkg/customAnalyzer/customAnalyzer.go @@ -0,0 +1,58 @@ +package customanalyzer + +import ( + "errors" + "fmt" + "reflect" + + "github.com/k8sgpt-ai/k8sgpt/pkg/customAnalyzer/docker" +) + +type CustomAnalyzerConfiguration struct { + Name string `mapstructure:"name"` + Connection Connection `mapstructure:"connection"` +} + +type Connection struct { + Url string `mapstructure:"url"` + Port int `mapstructure:"port"` +} + +type ICustomAnalyzer interface { + Deploy(packageUrl, name, url string, port int) error + UnDeploy(name string) error +} + +type CustomAnalyzer struct{} + +var customAnalyzerType = map[string]ICustomAnalyzer{ + "docker": docker.NewDocker(), +} + +func NewCustomAnalyzer() *CustomAnalyzer { + return &CustomAnalyzer{} +} + +func (*CustomAnalyzer) GetInstallType(name string) (ICustomAnalyzer, error) { + if _, ok := customAnalyzerType[name]; !ok { + return nil, errors.New("integration not found") + } + return customAnalyzerType[name], nil +} + +func (*CustomAnalyzer) Check(actualConfig []CustomAnalyzerConfiguration, name, url string, port int) error { + 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 +} diff --git a/pkg/customAnalyzer/docker/docker.go b/pkg/customAnalyzer/docker/docker.go new file mode 100644 index 0000000000..efc295af6c --- /dev/null +++ b/pkg/customAnalyzer/docker/docker.go @@ -0,0 +1,103 @@ +package docker + +import ( + "context" + "fmt" + "strconv" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" +) + +type Docker struct { + client client.Client + ctx context.Context +} + +func NewDocker() *Docker { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + + return &Docker{ + client: *cli, + ctx: context.Background(), + } +} + +func (d *Docker) Deploy(packageUrl, name, url string, port int) error { + portStr := strconv.Itoa(port) + containerPort := fmt.Sprintf("%s/tcp", portStr) + + config := &container.Config{ + Image: packageUrl, + ExposedPorts: nat.PortSet{ + nat.Port(containerPort): struct{}{}, + }, + } + + hostConfig := &container.HostConfig{ + PortBindings: nat.PortMap{ + nat.Port(containerPort): []nat.PortBinding{ + { + HostIP: url, + HostPort: portStr, + }, + }, + }, + } + + resp, err := d.client.ContainerCreate(d.ctx, config, hostConfig, nil, nil, name) + if err != nil { + return err + } + + if err := d.client.ContainerStart(d.ctx, resp.ID, container.StartOptions{}); err != nil { + return err + } + + return nil +} + +func (d *Docker) UnDeploy(name string) error { + timeout := 10 + + containerID, err := d.getContainerIDByName(name) + if err != nil { + return err + } + + if err := d.client.ContainerStop(d.ctx, containerID, container.StopOptions{Timeout: &timeout}); err != nil { + return err + } + + if err := d.client.ContainerRemove(d.ctx, containerID, container.RemoveOptions{}); err != nil { + return err + } + + return nil +} + +func (d *Docker) getContainerIDByName(containerName string) (string, error) { + filter := filters.NewArgs() + filter.Add("name", containerName) + var containerId string + + containers, err := d.client.ContainerList(d.ctx, container.ListOptions{ + All: true, + Filters: filter, + }) + + if err != nil { + return containerId, err + } + + if len(containers) == 0 { + return containerId, fmt.Errorf("no container found with %s name", containerName) + } + + return containers[0].ID, nil +} From 29133ba64a02343b23132249d5f0ac1b5ebeeb5c Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Sun, 21 Jul 2024 12:18:20 +0200 Subject: [PATCH 02/10] feat: enhance custom analyzer management with removal functionality Introduced the ability to remove custom analyzers, streamlining the management process and ensuring flexibility in custom analyzer configuration. This enhancement addresses the need for dynamic customization and maintenance of analyzer setups, facilitating easier updates and modifications to the analysis environment. Signed-off-by: Matthis Holleville --- cmd/customAnalyzer/add.go | 15 +++-- cmd/customAnalyzer/customAnalyzer.go | 4 +- cmd/customAnalyzer/remove.go | 99 ++++++++++++++++++++++++++++ pkg/customAnalyzer/customAnalyzer.go | 23 ++++++- 4 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 cmd/customAnalyzer/remove.go diff --git a/cmd/customAnalyzer/add.go b/cmd/customAnalyzer/add.go index 3f42a4dd0c..4fa76ba6e0 100644 --- a/cmd/customAnalyzer/add.go +++ b/cmd/customAnalyzer/add.go @@ -32,7 +32,7 @@ var ( port int ) -var AddCmd = &cobra.Command{ +var addCmd = &cobra.Command{ Use: "add", Aliases: []string{"add"}, Short: "This command will add a custom analyzer from source", @@ -80,6 +80,7 @@ var AddCmd = &cobra.Command{ Url: url, Port: port, }, + InstallType: installType, }) viper.Set("custom_analyzers", configCustomAnalyzer) @@ -93,10 +94,10 @@ var AddCmd = &cobra.Command{ } func init() { - AddCmd.Flags().StringVarP(&installType, "install-type", "t", "docker", "Specify the installation type (e.g., docker, kubernetes).") - AddCmd.Flags().BoolVarP(&install, "install", "i", false, "Flag to indicate whether to install the custom analyzer after adding.") - AddCmd.Flags().StringVarP(&packageUrl, "package", "p", "", "URL of the custom analyzer package.") - 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.") + addCmd.Flags().StringVarP(&installType, "install-type", "t", "docker", "Specify the installation type (e.g., docker, kubernetes).") + addCmd.Flags().BoolVarP(&install, "install", "i", false, "Flag to indicate whether to install the custom analyzer after adding.") + addCmd.Flags().StringVarP(&packageUrl, "package", "p", "", "URL of the custom analyzer package.") + 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 index 31679cd9a3..d734b884b5 100644 --- a/cmd/customAnalyzer/customAnalyzer.go +++ b/cmd/customAnalyzer/customAnalyzer.go @@ -35,5 +35,7 @@ var CustomAnalyzerCmd = &cobra.Command{ func init() { // add subcommand to add custom analyzer - CustomAnalyzerCmd.AddCommand(AddCmd) + CustomAnalyzerCmd.AddCommand(addCmd) + // remove subcomment to remove custom analyzer + CustomAnalyzerCmd.AddCommand(removeCmd) } diff --git a/cmd/customAnalyzer/remove.go b/cmd/customAnalyzer/remove.go new file mode 100644 index 0000000000..f19f905ccd --- /dev/null +++ b/cmd/customAnalyzer/remove.go @@ -0,0 +1,99 @@ +/* +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" + customanalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/customAnalyzer" + "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 + + // Attempt to undeploy the analyzer if install-type is set + customAnalyzer := customanalyzer.NewCustomAnalyzer() + err := customAnalyzer.UnDeploy(analyzer) + if err != nil { + // Warn if undeployment fails, but continue to remove the entry + color.Yellow("Warning: Unable to undeploy custom analyzer. Proceeding with orphan mode.") + } + + // 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/pkg/customAnalyzer/customAnalyzer.go b/pkg/customAnalyzer/customAnalyzer.go index 0b2d1b1137..2eaf4758db 100644 --- a/pkg/customAnalyzer/customAnalyzer.go +++ b/pkg/customAnalyzer/customAnalyzer.go @@ -9,8 +9,9 @@ import ( ) type CustomAnalyzerConfiguration struct { - Name string `mapstructure:"name"` - Connection Connection `mapstructure:"connection"` + Name string `mapstructure:"name"` + Connection Connection `mapstructure:"connection"` + InstallType string `mapstructure:"installtype,omitempty"` } type Connection struct { @@ -56,3 +57,21 @@ func (*CustomAnalyzer) Check(actualConfig []CustomAnalyzerConfiguration, name, u return nil } + +func (ca *CustomAnalyzer) UnDeploy(analyzer CustomAnalyzerConfiguration) error { + if analyzer.InstallType != "" { + // Try to undeploy if install-type is set + install, err := ca.GetInstallType(analyzer.InstallType) + if err != nil { + return err + } + + err = install.UnDeploy(analyzer.Name) + if err != nil { + return err + } + } + + return nil + +} From ae4f8c8d51a865dfab5b0a840554995dec54d55d Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Sun, 21 Jul 2024 12:24:58 +0200 Subject: [PATCH 03/10] feat: add list command to customAnalyzer for displaying configured analyzers Implemented a new list command within the customAnalyzer module to enable users to view all configured custom analyzers. This enhancement aims to improve usability by providing a straightforward method for users to inspect their custom analyzer configurations directly from the command line. Signed-off-by: Matthis Holleville --- cmd/customAnalyzer/customAnalyzer.go | 2 + cmd/customAnalyzer/list.go | 63 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 cmd/customAnalyzer/list.go diff --git a/cmd/customAnalyzer/customAnalyzer.go b/cmd/customAnalyzer/customAnalyzer.go index d734b884b5..9a88faebb8 100644 --- a/cmd/customAnalyzer/customAnalyzer.go +++ b/cmd/customAnalyzer/customAnalyzer.go @@ -38,4 +38,6 @@ func init() { 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..236db5a480 --- /dev/null +++ b/cmd/customAnalyzer/list.go @@ -0,0 +1,63 @@ +/* +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/customAnalyzer" + "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) { + if analyzer.InstallType != "" { + fmt.Printf(" - InstallType: %s\n", analyzer.InstallType) + } + fmt.Printf(" - Url: %s\n", analyzer.Connection.Url) + fmt.Printf(" - Port: %d\n", analyzer.Connection.Port) + +} From 644c35153e4de47802bc77b59539fe456f617122 Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Sun, 21 Jul 2024 12:29:20 +0200 Subject: [PATCH 04/10] feat: add support for listing, adding, and removing custom analyzers This update introduces commands to manage custom analyzers in the k8sgpt tool, enhancing flexibility and control over analyzer configurations without the need for direct installation or docker dependency. Signed-off-by: Matthis Holleville --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index d16f7ab0d9..b0440e1a99 100644 --- a/README.md +++ b/README.md @@ -504,6 +504,26 @@ 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 +``` + +_Adding custom analyzer from docker image_ +``` +k8sgpt custom-analyzer add --install --install-type docker --name my-custom-analyzer --package $MY_DOCKER_IMAGE --port 8085 +``` + +_Removing custom analyzer_ +``` +k8sgpt custom-analyzer remove --names "my-custom-analyzer,my-custom-analyzer-2" +``` + ## Documentation From 6c62558016f8fd3f5419b3b174fd113b27e3afd3 Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Mon, 22 Jul 2024 16:59:47 +0200 Subject: [PATCH 05/10] feat: support private docker image authentication for custom analyzers Added authentication support for pulling private Docker images when adding custom analyzers, enhancing security and access control. Signed-off-by: Matthis Holleville --- README.md | 4 ++-- cmd/customAnalyzer/add.go | 6 ++++- pkg/customAnalyzer/customAnalyzer.go | 2 +- pkg/customAnalyzer/docker/docker.go | 35 +++++++++++++++++++++++++++- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b0440e1a99..54081533dd 100644 --- a/README.md +++ b/README.md @@ -514,9 +514,9 @@ _Adding custom analyzer without install_ k8sgpt custom-analyzer add --name my-custom-analyzer --port 8085 ``` -_Adding custom analyzer from docker image_ +_Adding custom analyzer from docker image with auth_ ``` -k8sgpt custom-analyzer add --install --install-type docker --name my-custom-analyzer --package $MY_DOCKER_IMAGE --port 8085 +k8sgpt custom-analyzer add --install --install-type docker --name my-custom-analyzer --package $MY_PRIVATE_DOCKER_IMAGE --port 8085 --username $MY_USERNAME --pasword $MY_PASSWORD ``` _Removing custom analyzer_ diff --git a/cmd/customAnalyzer/add.go b/cmd/customAnalyzer/add.go index 4fa76ba6e0..49633f1582 100644 --- a/cmd/customAnalyzer/add.go +++ b/cmd/customAnalyzer/add.go @@ -29,6 +29,8 @@ var ( packageUrl string name string url string + username string + password string port int ) @@ -67,7 +69,7 @@ var addCmd = &cobra.Command{ } // create a pod in-cluster with custom analyzer - err = install.Deploy(packageUrl, name, url, port) + err = install.Deploy(packageUrl, name, url, username, password, port) if err != nil { color.Red("Error installing custom analyzer: %s", err.Error()) os.Exit(1) @@ -97,6 +99,8 @@ func init() { addCmd.Flags().StringVarP(&installType, "install-type", "t", "docker", "Specify the installation type (e.g., docker, kubernetes).") addCmd.Flags().BoolVarP(&install, "install", "i", false, "Flag to indicate whether to install the custom analyzer after adding.") addCmd.Flags().StringVarP(&packageUrl, "package", "p", "", "URL of the custom analyzer package.") + addCmd.Flags().StringVarP(&username, "username", "s", "", "Username used for pulling package.") + addCmd.Flags().StringVarP(&password, "password", "w", "", "Password used for pulling package.") 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/pkg/customAnalyzer/customAnalyzer.go b/pkg/customAnalyzer/customAnalyzer.go index 2eaf4758db..01df2a3de2 100644 --- a/pkg/customAnalyzer/customAnalyzer.go +++ b/pkg/customAnalyzer/customAnalyzer.go @@ -20,7 +20,7 @@ type Connection struct { } type ICustomAnalyzer interface { - Deploy(packageUrl, name, url string, port int) error + Deploy(packageUrl, name, url, username, password string, port int) error UnDeploy(name string) error } diff --git a/pkg/customAnalyzer/docker/docker.go b/pkg/customAnalyzer/docker/docker.go index efc295af6c..69567c6df6 100644 --- a/pkg/customAnalyzer/docker/docker.go +++ b/pkg/customAnalyzer/docker/docker.go @@ -2,11 +2,15 @@ package docker import ( "context" + "encoding/base64" + "encoding/json" "fmt" "strconv" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/registry" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" ) @@ -28,7 +32,31 @@ func NewDocker() *Docker { } } -func (d *Docker) Deploy(packageUrl, name, url string, port int) error { +func (d *Docker) pullImage(imageRef, username, password string) error { + authConfig := registry.AuthConfig{ + Username: username, + Password: password, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + panic(err) + } + authStr := base64.URLEncoding.EncodeToString(encodedJSON) + + _, _, err = d.client.ImageInspectWithRaw(d.ctx, imageRef) + if err != nil { + out, err := d.client.ImagePull(d.ctx, imageRef, image.PullOptions{RegistryAuth: authStr}) + if err != nil { + return err + } + defer out.Close() + } + + return nil + +} + +func (d *Docker) Deploy(packageUrl, name, url, username, password string, port int) error { portStr := strconv.Itoa(port) containerPort := fmt.Sprintf("%s/tcp", portStr) @@ -50,6 +78,11 @@ func (d *Docker) Deploy(packageUrl, name, url string, port int) error { }, } + err := d.pullImage(packageUrl, username, password) + if err != nil { + return err + } + resp, err := d.client.ContainerCreate(d.ctx, config, hostConfig, nil, nil, name) if err != nil { return err From 59f98bb645ece378a77099285d58f716220abb29 Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Mon, 5 Aug 2024 10:57:36 +0200 Subject: [PATCH 06/10] feat: remove Docker custom analyzer installation Removed the installation and deployment functionality for custom analyzers, streamlining the process of adding analyzers. This change focuses on simplifying the configuration by eliminating the need for specifying installation types, package URLs, and authentication details for Docker images. The goal is to enhance user experience by making the addition of custom analyzers more straightforward and less error-prone. Signed-off-by: Matthis Holleville --- README.md | 5 - cmd/customAnalyzer/add.go | 41 +------- cmd/customAnalyzer/list.go | 3 - cmd/customAnalyzer/remove.go | 9 -- pkg/customAnalyzer/customAnalyzer.go | 42 +-------- pkg/customAnalyzer/docker/docker.go | 136 --------------------------- 6 files changed, 6 insertions(+), 230 deletions(-) delete mode 100644 pkg/customAnalyzer/docker/docker.go diff --git a/README.md b/README.md index 54081533dd..d8ae91ed1b 100644 --- a/README.md +++ b/README.md @@ -514,11 +514,6 @@ _Adding custom analyzer without install_ k8sgpt custom-analyzer add --name my-custom-analyzer --port 8085 ``` -_Adding custom analyzer from docker image with auth_ -``` -k8sgpt custom-analyzer add --install --install-type docker --name my-custom-analyzer --package $MY_PRIVATE_DOCKER_IMAGE --port 8085 --username $MY_USERNAME --pasword $MY_PASSWORD -``` - _Removing custom analyzer_ ``` k8sgpt custom-analyzer remove --names "my-custom-analyzer,my-custom-analyzer-2" diff --git a/cmd/customAnalyzer/add.go b/cmd/customAnalyzer/add.go index 49633f1582..3449c209b5 100644 --- a/cmd/customAnalyzer/add.go +++ b/cmd/customAnalyzer/add.go @@ -14,7 +14,6 @@ limitations under the License. package customanalyzer import ( - "fmt" "os" "github.com/fatih/color" @@ -24,14 +23,10 @@ import ( ) var ( - installType string - install bool - packageUrl string - name string - url string - username string - password string - port int + packageUrl string + name string + url string + port int ) var addCmd = &cobra.Command{ @@ -39,12 +34,6 @@ var addCmd = &cobra.Command{ Aliases: []string{"add"}, Short: "This command will add a custom analyzer from source", Long: "This command allows you to add a custom analyzer from a specified source and optionally install it.", - PreRun: func(cmd *cobra.Command, args []string) { - if install { - _ = cmd.MarkFlagRequired("install-type") - _ = cmd.MarkFlagRequired("package") - } - }, Run: func(cmd *cobra.Command, args []string) { err := viper.UnmarshalKey("custom_analyzers", &configCustomAnalyzer) if err != nil { @@ -60,29 +49,12 @@ var addCmd = &cobra.Command{ os.Exit(1) } - if install { - // Check if installType is possible - install, err := customAnalyzer.GetInstallType(installType) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - // create a pod in-cluster with custom analyzer - err = install.Deploy(packageUrl, name, url, username, password, port) - if err != nil { - color.Red("Error installing custom analyzer: %s", err.Error()) - os.Exit(1) - } - } - configCustomAnalyzer = append(configCustomAnalyzer, customanalyzer.CustomAnalyzerConfiguration{ Name: name, Connection: customanalyzer.Connection{ Url: url, Port: port, }, - InstallType: installType, }) viper.Set("custom_analyzers", configCustomAnalyzer) @@ -96,11 +68,6 @@ var addCmd = &cobra.Command{ } func init() { - addCmd.Flags().StringVarP(&installType, "install-type", "t", "docker", "Specify the installation type (e.g., docker, kubernetes).") - addCmd.Flags().BoolVarP(&install, "install", "i", false, "Flag to indicate whether to install the custom analyzer after adding.") - addCmd.Flags().StringVarP(&packageUrl, "package", "p", "", "URL of the custom analyzer package.") - addCmd.Flags().StringVarP(&username, "username", "s", "", "Username used for pulling package.") - addCmd.Flags().StringVarP(&password, "password", "w", "", "Password used for pulling package.") 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/list.go b/cmd/customAnalyzer/list.go index 236db5a480..e87aca6156 100644 --- a/cmd/customAnalyzer/list.go +++ b/cmd/customAnalyzer/list.go @@ -54,9 +54,6 @@ func init() { } func printDetails(analyzer customanalyzer.CustomAnalyzerConfiguration) { - if analyzer.InstallType != "" { - fmt.Printf(" - InstallType: %s\n", analyzer.InstallType) - } 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 index f19f905ccd..f9f19471c9 100644 --- a/cmd/customAnalyzer/remove.go +++ b/cmd/customAnalyzer/remove.go @@ -18,7 +18,6 @@ import ( "strings" "github.com/fatih/color" - customanalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/customAnalyzer" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -61,14 +60,6 @@ var removeCmd = &cobra.Command{ if analyzer.Name == inputAnalyzer { foundAnalyzer = true - // Attempt to undeploy the analyzer if install-type is set - customAnalyzer := customanalyzer.NewCustomAnalyzer() - err := customAnalyzer.UnDeploy(analyzer) - if err != nil { - // Warn if undeployment fails, but continue to remove the entry - color.Yellow("Warning: Unable to undeploy custom analyzer. Proceeding with orphan mode.") - } - // 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) diff --git a/pkg/customAnalyzer/customAnalyzer.go b/pkg/customAnalyzer/customAnalyzer.go index 01df2a3de2..29e1b49dd0 100644 --- a/pkg/customAnalyzer/customAnalyzer.go +++ b/pkg/customAnalyzer/customAnalyzer.go @@ -1,17 +1,13 @@ package customanalyzer import ( - "errors" "fmt" "reflect" - - "github.com/k8sgpt-ai/k8sgpt/pkg/customAnalyzer/docker" ) type CustomAnalyzerConfiguration struct { - Name string `mapstructure:"name"` - Connection Connection `mapstructure:"connection"` - InstallType string `mapstructure:"installtype,omitempty"` + Name string `mapstructure:"name"` + Connection Connection `mapstructure:"connection"` } type Connection struct { @@ -19,28 +15,12 @@ type Connection struct { Port int `mapstructure:"port"` } -type ICustomAnalyzer interface { - Deploy(packageUrl, name, url, username, password string, port int) error - UnDeploy(name string) error -} - type CustomAnalyzer struct{} -var customAnalyzerType = map[string]ICustomAnalyzer{ - "docker": docker.NewDocker(), -} - func NewCustomAnalyzer() *CustomAnalyzer { return &CustomAnalyzer{} } -func (*CustomAnalyzer) GetInstallType(name string) (ICustomAnalyzer, error) { - if _, ok := customAnalyzerType[name]; !ok { - return nil, errors.New("integration not found") - } - return customAnalyzerType[name], nil -} - func (*CustomAnalyzer) Check(actualConfig []CustomAnalyzerConfiguration, name, url string, port int) error { for _, analyzer := range actualConfig { if analyzer.Name == name { @@ -57,21 +37,3 @@ func (*CustomAnalyzer) Check(actualConfig []CustomAnalyzerConfiguration, name, u return nil } - -func (ca *CustomAnalyzer) UnDeploy(analyzer CustomAnalyzerConfiguration) error { - if analyzer.InstallType != "" { - // Try to undeploy if install-type is set - install, err := ca.GetInstallType(analyzer.InstallType) - if err != nil { - return err - } - - err = install.UnDeploy(analyzer.Name) - if err != nil { - return err - } - } - - return nil - -} diff --git a/pkg/customAnalyzer/docker/docker.go b/pkg/customAnalyzer/docker/docker.go deleted file mode 100644 index 69567c6df6..0000000000 --- a/pkg/customAnalyzer/docker/docker.go +++ /dev/null @@ -1,136 +0,0 @@ -package docker - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "strconv" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" -) - -type Docker struct { - client client.Client - ctx context.Context -} - -func NewDocker() *Docker { - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - panic(err) - } - - return &Docker{ - client: *cli, - ctx: context.Background(), - } -} - -func (d *Docker) pullImage(imageRef, username, password string) error { - authConfig := registry.AuthConfig{ - Username: username, - Password: password, - } - encodedJSON, err := json.Marshal(authConfig) - if err != nil { - panic(err) - } - authStr := base64.URLEncoding.EncodeToString(encodedJSON) - - _, _, err = d.client.ImageInspectWithRaw(d.ctx, imageRef) - if err != nil { - out, err := d.client.ImagePull(d.ctx, imageRef, image.PullOptions{RegistryAuth: authStr}) - if err != nil { - return err - } - defer out.Close() - } - - return nil - -} - -func (d *Docker) Deploy(packageUrl, name, url, username, password string, port int) error { - portStr := strconv.Itoa(port) - containerPort := fmt.Sprintf("%s/tcp", portStr) - - config := &container.Config{ - Image: packageUrl, - ExposedPorts: nat.PortSet{ - nat.Port(containerPort): struct{}{}, - }, - } - - hostConfig := &container.HostConfig{ - PortBindings: nat.PortMap{ - nat.Port(containerPort): []nat.PortBinding{ - { - HostIP: url, - HostPort: portStr, - }, - }, - }, - } - - err := d.pullImage(packageUrl, username, password) - if err != nil { - return err - } - - resp, err := d.client.ContainerCreate(d.ctx, config, hostConfig, nil, nil, name) - if err != nil { - return err - } - - if err := d.client.ContainerStart(d.ctx, resp.ID, container.StartOptions{}); err != nil { - return err - } - - return nil -} - -func (d *Docker) UnDeploy(name string) error { - timeout := 10 - - containerID, err := d.getContainerIDByName(name) - if err != nil { - return err - } - - if err := d.client.ContainerStop(d.ctx, containerID, container.StopOptions{Timeout: &timeout}); err != nil { - return err - } - - if err := d.client.ContainerRemove(d.ctx, containerID, container.RemoveOptions{}); err != nil { - return err - } - - return nil -} - -func (d *Docker) getContainerIDByName(containerName string) (string, error) { - filter := filters.NewArgs() - filter.Add("name", containerName) - var containerId string - - containers, err := d.client.ContainerList(d.ctx, container.ListOptions{ - All: true, - Filters: filter, - }) - - if err != nil { - return containerId, err - } - - if len(containers) == 0 { - return containerId, fmt.Errorf("no container found with %s name", containerName) - } - - return containers[0].ID, nil -} From f59f8727ea7181d12759b975bc794eaf31f3603c Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Mon, 5 Aug 2024 12:20:33 +0200 Subject: [PATCH 07/10] fix: remove unused packageUrl Signed-off-by: Matthis Holleville --- cmd/customAnalyzer/add.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/customAnalyzer/add.go b/cmd/customAnalyzer/add.go index 3449c209b5..6907fe7802 100644 --- a/cmd/customAnalyzer/add.go +++ b/cmd/customAnalyzer/add.go @@ -23,10 +23,9 @@ import ( ) var ( - packageUrl string - name string - url string - port int + name string + url string + port int ) var addCmd = &cobra.Command{ From f137b60986d85194ba0394f38f23cd0cf73c0c89 Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Mon, 5 Aug 2024 19:36:04 +0200 Subject: [PATCH 08/10] feat: update add command description to reflect broader functionality Signed-off-by: Matthis Holleville --- cmd/customAnalyzer/add.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/customAnalyzer/add.go b/cmd/customAnalyzer/add.go index 6907fe7802..80aced1393 100644 --- a/cmd/customAnalyzer/add.go +++ b/cmd/customAnalyzer/add.go @@ -32,7 +32,7 @@ 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 a custom analyzer from a specified source and optionally install it.", + 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 { From 108424bc1ff733e94118d1dcc791182f8c1773f1 Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Mon, 5 Aug 2024 19:36:42 +0200 Subject: [PATCH 09/10] feat: Add name validation for custom analyzer creation To ensure the integrity and consistency of analyzer names, we introduced a validation step that checks the format of the name against a predefined regex pattern. This change aims to prevent the creation of analyzers with invalid names, enhancing the system's reliability and usability. Signed-off-by: Matthis Holleville --- pkg/customAnalyzer/customAnalyzer.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/customAnalyzer/customAnalyzer.go b/pkg/customAnalyzer/customAnalyzer.go index 29e1b49dd0..803386a76f 100644 --- a/pkg/customAnalyzer/customAnalyzer.go +++ b/pkg/customAnalyzer/customAnalyzer.go @@ -3,6 +3,7 @@ package customanalyzer import ( "fmt" "reflect" + "regexp" ) type CustomAnalyzerConfiguration struct { @@ -22,6 +23,12 @@ func NewCustomAnalyzer() *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) From 81ca4395f66c7b8d0999352b16bbab3bb88a9f6c Mon Sep 17 00:00:00 2001 From: Matthis Holleville Date: Mon, 5 Aug 2024 19:41:37 +0200 Subject: [PATCH 10/10] feat: refactor customAnalyzer package for consistent naming Refactored the customAnalyzer package and its references to use consistent snake_case naming for improved code readability and alignment with Go naming conventions. Signed-off-by: Matthis Holleville --- cmd/customAnalyzer/add.go | 10 +++++----- cmd/customAnalyzer/customAnalyzer.go | 4 ++-- cmd/customAnalyzer/list.go | 4 ++-- .../customAnalyzer.go | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) rename pkg/{customAnalyzer => custom_analyzer}/customAnalyzer.go (97%) diff --git a/cmd/customAnalyzer/add.go b/cmd/customAnalyzer/add.go index 80aced1393..91dbdb9896 100644 --- a/cmd/customAnalyzer/add.go +++ b/cmd/customAnalyzer/add.go @@ -17,7 +17,7 @@ import ( "os" "github.com/fatih/color" - customanalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/customAnalyzer" + customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -39,18 +39,18 @@ var addCmd = &cobra.Command{ color.Red("Error: %v", err) os.Exit(1) } - customAnalyzer := customanalyzer.NewCustomAnalyzer() + analyzer := customAnalyzer.NewCustomAnalyzer() // Check if configuration is valid - err = customAnalyzer.Check(configCustomAnalyzer, name, url, port) + 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{ + configCustomAnalyzer = append(configCustomAnalyzer, customAnalyzer.CustomAnalyzerConfiguration{ Name: name, - Connection: customanalyzer.Connection{ + Connection: customAnalyzer.Connection{ Url: url, Port: port, }, diff --git a/cmd/customAnalyzer/customAnalyzer.go b/cmd/customAnalyzer/customAnalyzer.go index 9a88faebb8..934ad7d4e2 100644 --- a/cmd/customAnalyzer/customAnalyzer.go +++ b/cmd/customAnalyzer/customAnalyzer.go @@ -14,11 +14,11 @@ limitations under the License. package customanalyzer import ( - customanalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/customAnalyzer" + customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer" "github.com/spf13/cobra" ) -var configCustomAnalyzer []customanalyzer.CustomAnalyzerConfiguration +var configCustomAnalyzer []customAnalyzer.CustomAnalyzerConfiguration // authCmd represents the auth command var CustomAnalyzerCmd = &cobra.Command{ diff --git a/cmd/customAnalyzer/list.go b/cmd/customAnalyzer/list.go index e87aca6156..d7027aa362 100644 --- a/cmd/customAnalyzer/list.go +++ b/cmd/customAnalyzer/list.go @@ -18,7 +18,7 @@ import ( "os" "github.com/fatih/color" - customanalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/customAnalyzer" + customAnalyzer "github.com/k8sgpt-ai/k8sgpt/pkg/custom_analyzer" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -53,7 +53,7 @@ func init() { listCmd.Flags().BoolVar(&details, "details", false, "Print custom analyzers configuration details") } -func printDetails(analyzer customanalyzer.CustomAnalyzerConfiguration) { +func printDetails(analyzer customAnalyzer.CustomAnalyzerConfiguration) { fmt.Printf(" - Url: %s\n", analyzer.Connection.Url) fmt.Printf(" - Port: %d\n", analyzer.Connection.Port) diff --git a/pkg/customAnalyzer/customAnalyzer.go b/pkg/custom_analyzer/customAnalyzer.go similarity index 97% rename from pkg/customAnalyzer/customAnalyzer.go rename to pkg/custom_analyzer/customAnalyzer.go index 803386a76f..ed3e03acce 100644 --- a/pkg/customAnalyzer/customAnalyzer.go +++ b/pkg/custom_analyzer/customAnalyzer.go @@ -1,4 +1,4 @@ -package customanalyzer +package custom_analyzer import ( "fmt" @@ -28,7 +28,7 @@ func (*CustomAnalyzer) Check(actualConfig []CustomAnalyzerConfiguration, name, u 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)