From 1ef7054e6c650804f2039acea13cdf41df6f7e78 Mon Sep 17 00:00:00 2001 From: Samantha Jayasinghe Date: Tue, 26 Mar 2024 11:08:11 +1300 Subject: [PATCH] feat: add custom resource definition(CRD) analyzer and validate conversion webhook Signed-off-by: Samantha Jayasinghe --- pkg/analyzer/analyzer.go | 1 + pkg/analyzer/crd.go | 122 +++++++++++++++++++++++++++++++++ pkg/analyzer/crd_test.go | 142 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 pkg/analyzer/crd.go create mode 100644 pkg/analyzer/crd_test.go diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index d63093c7e9..621aa20bb2 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -53,6 +53,7 @@ var additionalAnalyzerMap = map[string]common.IAnalyzer{ "GatewayClass": GatewayClassAnalyzer{}, "Gateway": GatewayAnalyzer{}, "HTTPRoute": HTTPRouteAnalyzer{}, + "Crd": CrdAnalyzer{}, } func ListFilters() ([]string, []string, []string) { diff --git a/pkg/analyzer/crd.go b/pkg/analyzer/crd.go new file mode 100644 index 0000000000..2719fec72f --- /dev/null +++ b/pkg/analyzer/crd.go @@ -0,0 +1,122 @@ +/* +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 analyzer + +import ( + "fmt" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + kind = "CustomResourceDefinition" + webhook = "Webhook" + serviceNotFound = "Custom Resource Definition Conversion Webhook Service %s not found" + apiSpecWebhookService = "spec.conversion.webhook.clientConfig.service" +) + +type CrdAnalyzer struct { +} + +func (CrdAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { + + apiDoc := kubernetes.K8sApiReference{ + Kind: kind, + ApiVersion: schema.GroupVersion{ + Group: "apps", + Version: "v1", + }, + OpenapiSchema: a.OpenapiSchema, + } + + AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ + "analyzer_name": kind, + }) + + var preAnalysis = map[string]common.PreAnalysis{} + + // Fetch all CRD's + client := a.Client.CtrlClient + crdList := &apiextensionsv1.CustomResourceDefinitionList{} + client.List(a.Context, &apiextensionsv1.CustomResourceDefinitionList{}) + if err := client.List(a.Context, crdList, &ctrl.ListOptions{}); err != nil { + return nil, err + } + + var failures []common.Failure + + for _, crd := range crdList.Items { + + // Check crd conversion webhook service + conversion := crd.Spec.Conversion + if conversion.Strategy == webhook && conversion.Webhook.ClientConfig.Service != nil { + + svc := crd.Spec.Conversion.Webhook.ClientConfig.Service + // Get the webhook service + _, err := a.Client.GetClient(). + CoreV1(). + Services(svc.Namespace). + Get(a.Context, svc.Name, v1.GetOptions{}) + if err != nil { + // If the service is not found, can't create the custom resource + failures = append(failures, common.Failure{ + Text: fmt.Sprintf(serviceNotFound, svc.Name), + KubernetesDoc: apiDoc.GetApiDocV2(apiSpecWebhookService), + Sensitive: []common.Sensitive{ + { + Unmasked: svc.Namespace, + Masked: util.MaskString(svc.Namespace), + }, + { + Unmasked: svc.Name, + Masked: util.MaskString(svc.Name), + }, + }, + }) + + AnalyzerErrorsMetric.WithLabelValues( + crd.Spec.Names.Singular, + svc.Name, + svc.Namespace, + ).Set(float64(len(failures))) + } + + } + + if len(failures) > 0 { + preAnalysis[fmt.Sprintf("%s", crd.Name)] = common.PreAnalysis{ + FailureDetails: failures, + } + + } + } + + for key, value := range preAnalysis { + var currentAnalysis = common.Result{ + Kind: kind, + Name: key, + Error: value.FailureDetails, + } + + a.Results = append(a.Results, currentAnalysis) + } + + return a.Results, nil +} diff --git a/pkg/analyzer/crd_test.go b/pkg/analyzer/crd_test.go new file mode 100644 index 0000000000..eabd1c5720 --- /dev/null +++ b/pkg/analyzer/crd_test.go @@ -0,0 +1,142 @@ +/* +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 analyzer + +import ( + "context" + "testing" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + "github.com/magiconair/properties/assert" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestCRDSuccess(t *testing.T) { + crd := apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "crdTest.stable.example.com", + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: "stable.example.com", + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{ + Name: "v1alpha1", + Served: true, + Storage: true, + }}, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "crdtests", + Singular: "crdtest", + Kind: "CrdTest", + }, + Scope: apiextensionsv1.ClusterScoped, + }, + } + + scheme := scheme.Scheme + + err := apiextensionsv1.AddToScheme(scheme) + if err != nil { + t.Error(err) + } + + objects := []runtime.Object{ + &crd, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := CrdAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + Config: nil, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 0) +} + +func TestCRDFailForConverstionWebhook(t *testing.T) { + crd := apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "crdTest.stable.example.com", + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: "stable.example.com", + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{ + Name: "v1alpha1", + Served: true, + Storage: true, + }}, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "crdtests", + Singular: "crdtest", + Kind: "CrdTest", + }, + Scope: apiextensionsv1.ClusterScoped, + Conversion: &apiextensionsv1.CustomResourceConversion{ + Strategy: "Webhook", + Webhook: &apiextensionsv1.WebhookConversion{ + ClientConfig: &apiextensionsv1.WebhookClientConfig{ + Service: &apiextensionsv1.ServiceReference{ + Name: "example-conversion-webhook-server", + Namespace: "default", + }, + }, + }, + }, + }, + } + + scheme := scheme.Scheme + + err := apiextensionsv1.AddToScheme(scheme) + if err != nil { + t.Error(err) + } + + objects := []runtime.Object{ + &crd, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := CrdAnalyzer{} + clientset := fake.NewSimpleClientset() + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientset, + CtrlClient: fakeClient, + Config: nil, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) +}