diff --git a/cmd/analyze/analyze.go b/cmd/analyze/analyze.go index a9b3cbe3a2..a0453b28ed 100644 --- a/cmd/analyze/analyze.go +++ b/cmd/analyze/analyze.go @@ -38,6 +38,8 @@ var ( withDoc bool interactiveMode bool customAnalysis bool + offlineMode bool + rcaPath string ) // AnalyzeCmd represents the problems command @@ -48,6 +50,10 @@ var AnalyzeCmd = &cobra.Command{ Long: `This command will find problems within your Kubernetes cluster and provide you with a list of issues that need to be resolved`, Run: func(cmd *cobra.Command, args []string) { + if offlineMode && rcaPath == "" { + color.Red("Offline mode of Analysis needs RCA path to be provided to extract the data") + os.Exit(1) + } // Create analysis configuration first. config, err := analysis.NewAnalysis( backend, @@ -59,6 +65,8 @@ var AnalyzeCmd = &cobra.Command{ maxConcurrency, withDoc, interactiveMode, + offlineMode, + rcaPath, ) if err != nil { @@ -139,4 +147,6 @@ func init() { // custom analysis flag AnalyzeCmd.Flags().BoolVarP(&customAnalysis, "custom-analysis", "z", false, "Enable custom analyzers") + AnalyzeCmd.Flags().BoolVar(&offlineMode, "offline-mode", false, "Run Analyzer in Offline mode from RCA collected data") + AnalyzeCmd.Flags().StringVar(&rcaPath, "rca-path", "", "Path Container RCA collected from RCA Collector infra") } diff --git a/pkg/analysis/analysis.go b/pkg/analysis/analysis.go index 17771233e8..f8678c64ce 100644 --- a/pkg/analysis/analysis.go +++ b/pkg/analysis/analysis.go @@ -18,6 +18,7 @@ import ( "encoding/base64" "errors" "fmt" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes/local" "reflect" "strings" "sync" @@ -79,11 +80,20 @@ func NewAnalysis( maxConcurrency int, withDoc bool, interactiveMode bool, + offlineMode bool, + rcaPath string, ) (*Analysis, error) { // Get kubernetes client from viper. kubecontext := viper.GetString("kubecontext") kubeconfig := viper.GetString("kubeconfig") - client, err := kubernetes.NewClient(kubecontext, kubeconfig) + var client *kubernetes.Client + var err error + if offlineMode { + client = &kubernetes.Client{} + client.Client, client.CtrlClient = local.GetLocalClient(rcaPath) + } else { + client, err = kubernetes.NewClient(kubecontext, kubeconfig) + } if err != nil { return nil, fmt.Errorf("initialising kubernetes client: %w", err) } diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index f3bb1d7dc2..d6d97da052 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -33,6 +33,14 @@ func (c *Client) GetCtrlClient() ctrl.Client { return c.CtrlClient } +func (c *Client) SetKubernetesClient(k kubernetes.Interface) { + c.Client = k +} + +func (c *Client) SetCtrlClient(k ctrl.Client) { + c.CtrlClient = k +} + func NewClient(kubecontext string, kubeconfig string) (*Client, error) { var config *rest.Config config, err := rest.InClusterConfig() diff --git a/pkg/kubernetes/local/kubernetes.go b/pkg/kubernetes/local/kubernetes.go new file mode 100644 index 0000000000..9b175a4ec6 --- /dev/null +++ b/pkg/kubernetes/local/kubernetes.go @@ -0,0 +1,153 @@ +package local + +import ( + "bufio" + "bytes" + "fmt" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" + clientGoScheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/testing" + "k8s.io/klog/v2" + "os" + "path/filepath" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" + fakeCtrlclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "strings" +) + +var sch *runtime.Scheme + +func findFilesForResource(rcaPath string, resource string, namespace string) []string { + resource = strings.ToLower(resource) + namespace = strings.ToLower(namespace) + globPattern := fmt.Sprintf("kubectl_get_%s_*-o_yaml.log", resource) + if namespace != "" { + globPattern = fmt.Sprintf("kubectl_get_%s_--namespace_%s_*-o_yaml.log", resource, namespace) + } + + files, err := filepath.Glob(rcaPath + "/" + globPattern) + if err != nil { + klog.ErrorS(err, "failed to determine the files container information for the resource", "resource", resource, "namespace", namespace, "pattern", globPattern) + return []string{} + } + return files +} + +func getResourceFromFileUsingGenerics[T runtime.Object](file string, objType T) error { + data, err := os.Open(file) + if err != nil { + return err + } + defer func() { + if err := data.Close(); err != nil { + klog.ErrorS(err, "failed to close file", "file", file) + } + }() + + scanner := bufio.NewScanner(data) + var buffer []byte + buf := bytes.NewBuffer(buffer) + writer := bufio.NewWriter(buf) + for scanner.Scan() { + l := scanner.Bytes() + if string(l) == "---------------------------------------------------------------------" { + break + } + if _, err := writer.WriteString(fmt.Sprintf("%s\n", string(l))); err != nil { + return err + } + } + if err := writer.Flush(); err != nil { + return err + } + + decoder := yaml.NewYAMLOrJSONDecoder(bufio.NewReader(bytes.NewBuffer(buf.Bytes())), 100) + if err := decoder.Decode(objType); err != nil { + return err + } + return nil +} + +func GenericFetcher[T runtime.Object](objType T, gvk schema.GroupVersionKind, resourceKind string, rcaPath string, action testing.Action) (bool, []T, error) { + files := findFilesForResource(rcaPath, resourceKind, action.GetNamespace()) + list := &unstructured.UnstructuredList{ + Items: make([]unstructured.Unstructured, 0), + } + var items []T + if len(files) > 0 { + for _, file := range files { + err := getResourceFromFileUsingGenerics(file, list) + if err != nil { + return true, nil, err + } + for _, d := range list.Items { + d.SetGroupVersionKind(gvk) + if err := sch.Convert(&d, objType, nil); err != nil { + return true, nil, err + } + if action.GetVerb() == "get" && d.GetName() == action.(testing.GetAction).GetName() && d.GetNamespace() == action.(testing.GetAction).GetNamespace() { + d.SetGroupVersionKind(gvk) + return true, []T{objType}, nil + } + items = append(items, objType) + } + } + } + if len(files) == 0 && action.GetVerb() == "get" { + return true, nil, errors.NewNotFound(action.GetResource().GroupResource(), action.(testing.GetAction).GetName()) + } + return true, items, nil +} + +func GetLocalClient(rcaPath string) (kubernetes.Interface, ctrl.Client) { + sch = runtime.NewScheme() + _ = scheme.AddToScheme(sch) + _ = apiextensionsv1.AddToScheme(sch) + + _ = clientGoScheme.AddToScheme(sch) + fakeClient := fake.NewSimpleClientset() + + fakeClient.PrependReactor("list", "deployments", func(action testing.Action) (handled bool, ret runtime.Object, err error) { + handled, items, err := GenericFetcher(&appsv1.Deployment{}, schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, "deployments", rcaPath, action) + if err != nil { + return handled, nil, err + } + deployments := appsv1.DeploymentList{Items: make([]appsv1.Deployment, 0)} + for _, item := range items { + deployments.Items = append(deployments.Items, *item) + } + return handled, &deployments, nil + }) + + fakeClient.PrependReactor("get", "deployments", func(action testing.Action) (handled bool, ret runtime.Object, err error) { + handled, items, err := GenericFetcher(&appsv1.Deployment{}, schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, "deployments", rcaPath, action) + if err != nil { + return handled, nil, err + } + return handled, items[0], nil + }) + + fakeClient.PrependReactor("list", "nodes", func(action testing.Action) (handled bool, ret runtime.Object, err error) { + handled, items, err := GenericFetcher(&corev1.Node{}, schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}, "nodes", rcaPath, action) + if err != nil { + return handled, nil, err + } + nodes := corev1.NodeList{Items: make([]corev1.Node, 0)} + for _, item := range items { + nodes.Items = append(nodes.Items, *item) + } + return handled, &nodes, nil + }) + + return fakeClient, fakeCtrlclient.NewClientBuilder().WithScheme(sch).WithRuntimeObjects().Build() +} diff --git a/pkg/server/analyze.go b/pkg/server/analyze.go index edd4dafaa0..f315c726d6 100644 --- a/pkg/server/analyze.go +++ b/pkg/server/analyze.go @@ -29,7 +29,9 @@ func (h *handler) Analyze(ctx context.Context, i *schemav1.AnalyzeRequest) ( i.Explain, int(i.MaxConcurrency), false, // Kubernetes Doc disabled in server mode - false, // Interactive mode disabled in server mode + false, // Interactive mode disabled in server mode, + false, + "", ) config.Context = ctx // Replace context for correct timeouts. if err != nil {