From 58a76a94d8309ae158f641adba3c7aa1a1c4620d Mon Sep 17 00:00:00 2001 From: JuHyung Son Date: Thu, 18 Jul 2024 22:31:41 +0900 Subject: [PATCH] feat: add label selector (#1201) * feat: fix the custom-analysis printing (#1195) Signed-off-by: Alex Jones Signed-off-by: JuHyung-Son * feat: add label selector Signed-off-by: JuHyung-Son * feat: add label selector on analyzers Signed-off-by: JuHyung-Son * chore(deps): pin goreleaser/goreleaser-action action to 286f3b1 (#1171) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: JuHyung-Son * fix(deps): update module buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go to v1.4.0-20240715142657-3785f0a44aae.2 (#1196) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: JuHyung-Son * chore(deps): update actions/upload-artifact digest to 0b2256b (#1175) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: JuHyung-Son * chore: update proto pkg version Signed-off-by: JuHyung-Son * chore: fix typo Signed-off-by: JuHyung-Son * feat: add label string to LabelSelector util func Signed-off-by: JuHyung-Son * feat: add test using 2 label selector Signed-off-by: JuHyung-Son --------- Signed-off-by: Alex Jones Signed-off-by: JuHyung-Son Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: JuHyung Son Co-authored-by: Alex Jones Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Signed-off-by: AlexsJones --- cmd/analyze/analyze.go | 4 + go.mod | 4 +- go.sum | 16 +++- pkg/analysis/analysis.go | 4 + pkg/analyzer/cronjob.go | 2 +- pkg/analyzer/cronjob_test.go | 42 ++++++++++ pkg/analyzer/deployment.go | 2 +- pkg/analyzer/deployment_test.go | 52 ++++++++++++ pkg/analyzer/gateway.go | 4 +- pkg/analyzer/gateway_test.go | 78 +++++++++++++++++- pkg/analyzer/gatewayclass.go | 4 +- pkg/analyzer/gatewayclass_test.go | 48 +++++++++++ pkg/analyzer/hpa.go | 2 +- pkg/analyzer/hpaAnalyzer_test.go | 34 ++++++++ pkg/analyzer/httproute.go | 4 +- pkg/analyzer/ingress.go | 2 +- pkg/analyzer/ingress_test.go | 37 +++++++++ pkg/analyzer/log.go | 2 +- pkg/analyzer/log_test.go | 53 ++++++++++++ pkg/analyzer/mutating_webhook.go | 2 +- pkg/analyzer/mutating_webhook_test.go | 75 +++++++++++++++++ pkg/analyzer/netpol.go | 2 +- pkg/analyzer/netpol_test.go | 43 ++++++++++ pkg/analyzer/node.go | 2 +- pkg/analyzer/node_test.go | 48 +++++++++++ pkg/analyzer/pdb.go | 2 +- pkg/analyzer/pdb_test.go | 91 +++++++++++++++++++++ pkg/analyzer/pod.go | 4 +- pkg/analyzer/pvc.go | 2 +- pkg/analyzer/pvc_test.go | 50 ++++++++++++ pkg/analyzer/rs.go | 2 +- pkg/analyzer/rs_test.go | 55 +++++++++++++ pkg/analyzer/service.go | 2 +- pkg/analyzer/service_test.go | 103 ++++++++++++++++++++++++ pkg/analyzer/statefulset.go | 2 +- pkg/analyzer/statefulset_test.go | 52 ++++++++++++ pkg/analyzer/validating_webhook.go | 2 +- pkg/analyzer/validating_webhook_test.go | 77 ++++++++++++++++++ pkg/common/types.go | 1 + pkg/server/analyze.go | 1 + pkg/util/util.go | 16 ++++ 41 files changed, 1000 insertions(+), 28 deletions(-) diff --git a/cmd/analyze/analyze.go b/cmd/analyze/analyze.go index 69c5dbb2e5..0be570ba70 100644 --- a/cmd/analyze/analyze.go +++ b/cmd/analyze/analyze.go @@ -33,6 +33,7 @@ var ( language string nocache bool namespace string + labelSelector string anonymize bool maxConcurrency int withDoc bool @@ -55,6 +56,7 @@ var AnalyzeCmd = &cobra.Command{ language, filters, namespace, + labelSelector, nocache, explain, maxConcurrency, @@ -142,4 +144,6 @@ func init() { AnalyzeCmd.Flags().BoolVarP(&customAnalysis, "custom-analysis", "z", false, "Enable custom analyzers") // add custom headers flag AnalyzeCmd.Flags().StringSliceVarP(&customHeaders, "custom-headers", "r", []string{}, "Custom Headers, : (e.g CustomHeaderKey:CustomHeaderValue AnotherHeader:AnotherValue)") + // label selector flag + AnalyzeCmd.Flags().StringVarP(&labelSelector, "selector", "L", "", "Label selector (label query) to filter on, supports '=', '==', and '!='. (e.g. -L key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") } diff --git a/go.mod b/go.mod index ec4c896f1f..aa28c20151 100644 --- a/go.mod +++ b/go.mod @@ -29,8 +29,8 @@ require github.com/adrg/xdg v0.4.0 require ( buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc-ecosystem/gateway/v2 v2.20.0-20240406062209-1cc152efbf5c.1 - buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.4.0-20240715142657-3785f0a44aae.2 - buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.2-20240715142657-3785f0a44aae.2 + buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.3.0-20240406062209-1cc152efbf5c.3 + buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.2-20240717144446-c4efcc29ff16.2 cloud.google.com/go/storage v1.40.0 cloud.google.com/go/vertexai v0.7.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 diff --git a/go.sum b/go.sum index 8b2f2aa0cc..dc737049ac 100644 --- a/go.sum +++ b/go.sum @@ -8,13 +8,20 @@ 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.4.0-20240715142657-3785f0a44aae.2 h1:cUwK1ZqeW7HBL/kVxp16XHz7zXzVsuwXZQBKqAgh35k= -buf.build/gen/go/k8sgpt-ai/k8sgpt/grpc/go v1.4.0-20240715142657-3785f0a44aae.2/go.mod h1:/amuehNhXZEybzOmSYq4ghCe+4j9IoMaQldDOOPsPL4= +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.2-20240715142657-3785f0a44aae.2 h1:rH3TEq0JwQ0AzP3dGkPMwzrjOg3F+548ph7RgSvw2aQ= -buf.build/gen/go/k8sgpt-ai/k8sgpt/protocolbuffers/go v1.34.2-20240715142657-3785f0a44aae.2/go.mod h1:1wq1qVxvJkTEUQsF5/XjmhQYXYhbVoLSGhKnzS3ie54= +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= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -3315,6 +3322,7 @@ 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/analysis/analysis.go b/pkg/analysis/analysis.go index cf4163d5e4..d8c9b59d40 100644 --- a/pkg/analysis/analysis.go +++ b/pkg/analysis/analysis.go @@ -44,6 +44,7 @@ type Analysis struct { Results []common.Result Errors []string Namespace string + LabelSelector string Cache cache.ICache Explain bool MaxConcurrency int @@ -74,6 +75,7 @@ func NewAnalysis( language string, filters []string, namespace string, + labelSelector string, noCache bool, explain bool, maxConcurrency int, @@ -105,6 +107,7 @@ func NewAnalysis( Client: client, Language: language, Namespace: namespace, + LabelSelector: labelSelector, Cache: cache, Explain: explain, MaxConcurrency: maxConcurrency, @@ -200,6 +203,7 @@ func (a *Analysis) RunAnalysis() { Client: a.Client, Context: a.Context, Namespace: a.Namespace, + LabelSelector: a.LabelSelector, AIClient: a.AIClient, OpenapiSchema: openapiSchema, } diff --git a/pkg/analyzer/cronjob.go b/pkg/analyzer/cronjob.go index 866b2ea79e..c79ae2fa90 100644 --- a/pkg/analyzer/cronjob.go +++ b/pkg/analyzer/cronjob.go @@ -43,7 +43,7 @@ func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, err "analyzer_name": kind, }) - cronJobList, err := a.Client.GetClient().BatchV1().CronJobs(a.Namespace).List(a.Context, v1.ListOptions{}) + cronJobList, err := a.Client.GetClient().BatchV1().CronJobs(a.Namespace).List(a.Context, v1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/cronjob_test.go b/pkg/analyzer/cronjob_test.go index 639a71658e..d11a33290a 100644 --- a/pkg/analyzer/cronjob_test.go +++ b/pkg/analyzer/cronjob_test.go @@ -156,3 +156,45 @@ func TestCronJobAnalyzer(t *testing.T) { require.Equal(t, expectations[i], result.Name) } } + +func TestCronJobAnalyzerLabelSelectorFiltering(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", + Namespace: "default", + Labels: map[string]string{ + "app": "cronjob", + }, + }, + }, + &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: "CJ2", + Namespace: "default", + }, + }, + ), + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=cronjob", + } + + cjAnalyzer := CronJobAnalyzer{} + results, err := cjAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "default/CJ1", results[0].Name) +} diff --git a/pkg/analyzer/deployment.go b/pkg/analyzer/deployment.go index d2d7b19ef1..3574028b46 100644 --- a/pkg/analyzer/deployment.go +++ b/pkg/analyzer/deployment.go @@ -46,7 +46,7 @@ func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) "analyzer_name": kind, }) - deployments, err := a.Client.GetClient().AppsV1().Deployments(a.Namespace).List(context.Background(), v1.ListOptions{}) + deployments, err := a.Client.GetClient().AppsV1().Deployments(a.Namespace).List(context.Background(), v1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/deployment_test.go b/pkg/analyzer/deployment_test.go index 6a15507533..d7486351be 100644 --- a/pkg/analyzer/deployment_test.go +++ b/pkg/analyzer/deployment_test.go @@ -151,3 +151,55 @@ func TestDeploymentAnalyzerNamespaceFiltering(t *testing.T) { assert.Equal(t, analysisResults[0].Kind, "Deployment") assert.Equal(t, analysisResults[0].Name, "default/example") } + +func TestDeploymentAnalyzerLabelSelectorFiltering(t *testing.T) { + clientset := fake.NewSimpleClientset( + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: "default", + Labels: map[string]string{ + "app": "deployment", + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: func() *int32 { i := int32(3); return &i }(), + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + Containers: []v1.Container{}, + }, + }, + }, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example2", + Namespace: "default", + }, + Spec: appsv1.DeploymentSpec{ + Replicas: func() *int32 { i := int32(3); return &i }(), + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + Containers: []v1.Container{}, + }, + }, + }, + }, + ) + + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientset, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=deployment", + } + + deploymentAnalyzer := DeploymentAnalyzer{} + analysisResults, err := deploymentAnalyzer.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) +} diff --git a/pkg/analyzer/gateway.go b/pkg/analyzer/gateway.go index 557af26dd2..1ff1b2963c 100644 --- a/pkg/analyzer/gateway.go +++ b/pkg/analyzer/gateway.go @@ -41,7 +41,9 @@ func (GatewayAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { if err != nil { return nil, err } - if err := client.List(a.Context, gtwList, &ctrl.ListOptions{}); err != nil { + + labelSelector := util.LabelStrToSelector(a.LabelSelector) + if err := client.List(a.Context, gtwList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil { return nil, err } diff --git a/pkg/analyzer/gateway_test.go b/pkg/analyzer/gateway_test.go index 57deea3e34..db24bc0fe3 100644 --- a/pkg/analyzer/gateway_test.go +++ b/pkg/analyzer/gateway_test.go @@ -25,10 +25,13 @@ func BuildGatewayClass(name string) gtwapi.GatewayClass { return GatewayClass } -func BuildGateway(className gtwapi.ObjectName, status metav1.ConditionStatus) gtwapi.Gateway { +func BuildGateway(className gtwapi.ObjectName, status metav1.ConditionStatus, labels map[string]string) gtwapi.Gateway { Gateway := gtwapi.Gateway{} Gateway.Name = "foobar" Gateway.Namespace = "default" + if labels != nil { + Gateway.Labels = labels + } Gateway.Spec.GatewayClassName = className Gateway.Spec.Listeners = []gtwapi.Listener{ { @@ -53,7 +56,7 @@ func TestGatewayAnalyzer(t *testing.T) { AcceptedStatus := metav1.ConditionTrue GatewayClass := BuildGatewayClass(string(ClassName)) - Gateway := BuildGateway(ClassName, AcceptedStatus) + Gateway := BuildGateway(ClassName, AcceptedStatus, nil) // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme @@ -91,7 +94,7 @@ func TestGatewayAnalyzer(t *testing.T) { func TestMissingClassGatewayAnalyzer(t *testing.T) { ClassName := gtwapi.ObjectName("non-existed") AcceptedStatus := metav1.ConditionTrue - Gateway := BuildGateway(ClassName, AcceptedStatus) + Gateway := BuildGateway(ClassName, AcceptedStatus, nil) // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme @@ -130,7 +133,7 @@ func TestStatusGatewayAnalyzer(t *testing.T) { AcceptedStatus := metav1.ConditionUnknown GatewayClass := BuildGatewayClass(string(ClassName)) - Gateway := BuildGateway(ClassName, AcceptedStatus) + Gateway := BuildGateway(ClassName, AcceptedStatus, nil) // Create a Gateway Analyzer instance with the fake client scheme := scheme.Scheme @@ -178,3 +181,70 @@ func TestStatusGatewayAnalyzer(t *testing.T) { t.Errorf("Expected message, <%v> , not found in Gateway's analysis results", want) } } + +func TestGatewayAnalyzerLabelSelectorFiltering(t *testing.T) { + ClassName := gtwapi.ObjectName("non-existed") + AcceptedStatus := metav1.ConditionTrue + + Gateway := BuildGateway(ClassName, AcceptedStatus, map[string]string{"app": "gateway"}) + scheme := scheme.Scheme + err := gtwapi.Install(scheme) + if err != nil { + t.Error(err) + } + err = apiextensionsv1.AddToScheme(scheme) + if err != nil { + t.Error(err) + } + objects := []runtime.Object{ + &Gateway, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := GatewayAnalyzer{} + // without label selector should return 1 result + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) + + // with label selector should return 1 result + config = common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=gateway", + } + analysisResults, err = analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) + + // with wrong label selector should return 0 result + config = common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=wrong", + } + analysisResults, err = analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 0) + +} diff --git a/pkg/analyzer/gatewayclass.go b/pkg/analyzer/gatewayclass.go index fb516766a5..40b8ca7c12 100644 --- a/pkg/analyzer/gatewayclass.go +++ b/pkg/analyzer/gatewayclass.go @@ -39,7 +39,9 @@ func (GatewayClassAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) if err != nil { return nil, err } - if err := client.List(a.Context, gcList, &ctrl.ListOptions{}); err != nil { + + labelSelector := util.LabelStrToSelector(a.LabelSelector) + if err := client.List(a.Context, gcList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} diff --git a/pkg/analyzer/gatewayclass_test.go b/pkg/analyzer/gatewayclass_test.go index 37a40951ee..dc106db845 100644 --- a/pkg/analyzer/gatewayclass_test.go +++ b/pkg/analyzer/gatewayclass_test.go @@ -55,3 +55,51 @@ func TestGatewayClassAnalyzer(t *testing.T) { assert.Equal(t, len(analysisResults), 1) } + +func TestGatewayClassAnalyzerLabelSelectorFiltering(t *testing.T) { + condition := metav1.Condition{ + Type: "Accepted", + Status: "Ready", + Message: "Ready", + Reason: "Ready", + } + + // Create two GatewayClasses with different labels + GatewayClass := >wapi.GatewayClass{} + GatewayClass.Name = "foobar" + GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller" + GatewayClass.Labels = map[string]string{"app": "gatewayclass"} + GatewayClass.Status.Conditions = []metav1.Condition{condition} + + GatewayClass2 := >wapi.GatewayClass{} + GatewayClass2.Name = "foobar2" + GatewayClass2.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller" + GatewayClass2.Status.Conditions = []metav1.Condition{condition} + + scheme := scheme.Scheme + err := gtwapi.Install(scheme) + if err != nil { + t.Error(err) + } + err = apiextensionsv1.AddToScheme(scheme) + if err != nil { + t.Error(err) + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(GatewayClass, GatewayClass2).Build() + + analyzerInstance := GatewayClassAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=gatewayclass", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) +} diff --git a/pkg/analyzer/hpa.go b/pkg/analyzer/hpa.go index c70b93cc3a..0606d00c22 100644 --- a/pkg/analyzer/hpa.go +++ b/pkg/analyzer/hpa.go @@ -43,7 +43,7 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { "analyzer_name": kind, }) - list, err := a.Client.GetClient().AutoscalingV1().HorizontalPodAutoscalers(a.Namespace).List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().AutoscalingV1().HorizontalPodAutoscalers(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/hpaAnalyzer_test.go b/pkg/analyzer/hpaAnalyzer_test.go index 68055e1432..2be0d1049a 100644 --- a/pkg/analyzer/hpaAnalyzer_test.go +++ b/pkg/analyzer/hpaAnalyzer_test.go @@ -531,3 +531,37 @@ func TestHPAAnalyzerNamespaceFiltering(t *testing.T) { } assert.Equal(t, len(analysisResults), 1) } + +func TestHPAAnalyzerLabelSelectorFiltering(t *testing.T) { + clientset := fake.NewSimpleClientset( + &autoscalingv1.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: "default", + Labels: map[string]string{ + "app": "hpa", + }, + }, + }, + &autoscalingv1.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example2", + Namespace: "default", + }, + }, + ) + hpaAnalyzer := HpaAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientset, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=hpa", + } + analysisResults, err := hpaAnalyzer.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) +} diff --git a/pkg/analyzer/httproute.go b/pkg/analyzer/httproute.go index 18cfebb944..bb1aa48c92 100644 --- a/pkg/analyzer/httproute.go +++ b/pkg/analyzer/httproute.go @@ -42,7 +42,9 @@ func (HTTPRouteAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { if err != nil { return nil, err } - if err := client.List(a.Context, routeList, &ctrl.ListOptions{}); err != nil { + + labelSelector := util.LabelStrToSelector(a.LabelSelector) + if err := client.List(a.Context, routeList, &ctrl.ListOptions{LabelSelector: labelSelector}); err != nil { return nil, err } var preAnalysis = map[string]common.PreAnalysis{} diff --git a/pkg/analyzer/ingress.go b/pkg/analyzer/ingress.go index 648266f014..90bc542380 100644 --- a/pkg/analyzer/ingress.go +++ b/pkg/analyzer/ingress.go @@ -41,7 +41,7 @@ func (IngressAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { "analyzer_name": kind, }) - list, err := a.Client.GetClient().NetworkingV1().Ingresses(a.Namespace).List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().NetworkingV1().Ingresses(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/ingress_test.go b/pkg/analyzer/ingress_test.go index c5eb675aed..c9eb157807 100644 --- a/pkg/analyzer/ingress_test.go +++ b/pkg/analyzer/ingress_test.go @@ -201,3 +201,40 @@ func TestIngressAnalyzer(t *testing.T) { require.Equal(t, expectations[i].failuresCount, len(result.Error)) } } + +func TestIngressAnalyzerLabelSelectorFiltering(t *testing.T) { + validIgClassName := new(string) + *validIgClassName = "valid-ingress-class" + + 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", + }, + }, + ), + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=ingress", + } + + igAnalyzer := IngressAnalyzer{} + results, err := igAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "default/Ingress1", results[0].Name) + +} diff --git a/pkg/analyzer/log.go b/pkg/analyzer/log.go index eb5403b1b7..b6f2823357 100644 --- a/pkg/analyzer/log.go +++ b/pkg/analyzer/log.go @@ -41,7 +41,7 @@ func (LogAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { }) // search all namespaces for pods that are not running - list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/log_test.go b/pkg/analyzer/log_test.go index 0829e584f4..03e09d3f91 100644 --- a/pkg/analyzer/log_test.go +++ b/pkg/analyzer/log_test.go @@ -118,3 +118,56 @@ func TestLogAnalyzer(t *testing.T) { } } } + +func TestLogAnalyzerLabelSelectorFiltering(t *testing.T) { + oldPattern := errorPattern + errorPattern = regexp.MustCompile(`(fake logs)`) + t.Cleanup(func() { + errorPattern = oldPattern + }) + + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: fake.NewSimpleClientset( + &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Pod1", + Namespace: "default", + Labels: map[string]string{ + "app": "log", + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "test-container1", + }, + }, + }, + }, + &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Pod2", + Namespace: "default", + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "test-container2", + }, + }, + }, + }, + ), + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=log", + } + + logAnalyzer := LogAnalyzer{} + results, err := logAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "default/Pod1/test-container1", results[0].Name) +} diff --git a/pkg/analyzer/mutating_webhook.go b/pkg/analyzer/mutating_webhook.go index fa12e2ba2f..38f5f7c021 100644 --- a/pkg/analyzer/mutating_webhook.go +++ b/pkg/analyzer/mutating_webhook.go @@ -42,7 +42,7 @@ func (MutatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, erro "analyzer_name": kind, }) - mutatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), v1.ListOptions{}) + mutatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), v1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/mutating_webhook_test.go b/pkg/analyzer/mutating_webhook_test.go index a5b9d7d211..34c336dd2c 100644 --- a/pkg/analyzer/mutating_webhook_test.go +++ b/pkg/analyzer/mutating_webhook_test.go @@ -138,3 +138,78 @@ func TestMutatingWebhookAnalyzer(t *testing.T) { resultsLen := 3 require.Equal(t, resultsLen, len(results)) } + +func TestMutatingWebhookAnalyzerLabelSelectorFiltering(t *testing.T) { + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: fake.NewSimpleClientset( + &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Pod1", + Namespace: "default", + Labels: map[string]string{ + "app": "mutating-webhook", + }, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service1", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app": "mutating-webhook", + }, + }, + }, + &admissionregistrationv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-mutating-webhook-config", + Namespace: "default", + Labels: map[string]string{ + "app": "mutating-webhook", + }, + }, + Webhooks: []admissionregistrationv1.MutatingWebhook{ + { + Name: "webhook1", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: "test-service1", + Namespace: "default", + }, + }, + }, + }, + }, + &admissionregistrationv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-mutating-webhook-config2", + Namespace: "default", + }, + Webhooks: []admissionregistrationv1.MutatingWebhook{ + { + Name: "webhook2", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: "test-service1", + Namespace: "default", + }, + }, + }, + }, + }, + ), + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=mutating-webhook", + } + + mwAnalyzer := MutatingWebhookAnalyzer{} + results, err := mwAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "default/webhook1", results[0].Name) +} diff --git a/pkg/analyzer/netpol.go b/pkg/analyzer/netpol.go index aeb302dcd8..0ac5a6ba08 100644 --- a/pkg/analyzer/netpol.go +++ b/pkg/analyzer/netpol.go @@ -43,7 +43,7 @@ func (NetworkPolicyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) // get all network policies in the namespace policies, err := a.Client.GetClient().NetworkingV1(). - NetworkPolicies(a.Namespace).List(a.Context, metav1.ListOptions{}) + NetworkPolicies(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/netpol_test.go b/pkg/analyzer/netpol_test.go index b06318e187..1b57cdfb72 100644 --- a/pkg/analyzer/netpol_test.go +++ b/pkg/analyzer/netpol_test.go @@ -220,3 +220,46 @@ func TestNetpolNoPodsNamespaceFiltering(t *testing.T) { assert.Equal(t, results[0].Kind, "NetworkPolicy") } + +func TestNetpolLabelSelectorFiltering(t *testing.T) { + clientset := fake.NewSimpleClientset( + &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example1", + Namespace: "default", + Labels: map[string]string{ + "app": "netpol", + }, + }, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "netpol", + }, + }, + }, + }, + &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example2", + Namespace: "default", + }, + }, + ) + + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientset, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=netpol", + } + + analyzer := NetworkPolicyAnalyzer{} + results, err := analyzer.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(results), 1) +} diff --git a/pkg/analyzer/node.go b/pkg/analyzer/node.go index 5c9bef04de..49f3fcc6ac 100644 --- a/pkg/analyzer/node.go +++ b/pkg/analyzer/node.go @@ -33,7 +33,7 @@ func (NodeAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { "analyzer_name": kind, }) - list, err := a.Client.GetClient().CoreV1().Nodes().List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().CoreV1().Nodes().List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/node_test.go b/pkg/analyzer/node_test.go index 5c54e268c7..a008fc09f2 100644 --- a/pkg/analyzer/node_test.go +++ b/pkg/analyzer/node_test.go @@ -167,3 +167,51 @@ func TestNodeAnalyzer(t *testing.T) { require.Equal(t, expectations[i].failuresCount, len(result.Error)) } } + +func TestNodeAnalyzerLabelSelectorFiltering(t *testing.T) { + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Node1", + Namespace: "default", + Labels: map[string]string{ + "app": "node", + }, + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionFalse, + }, + }, + }, + }, + &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Node2", + Namespace: "default", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionFalse, + }, + }, + }, + }, + ), + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=node", + } + + nAnalyzer := NodeAnalyzer{} + results, err := nAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "Node1", results[0].Name) +} diff --git a/pkg/analyzer/pdb.go b/pkg/analyzer/pdb.go index 5ef2fba5d0..e5ce5d5dbb 100644 --- a/pkg/analyzer/pdb.go +++ b/pkg/analyzer/pdb.go @@ -41,7 +41,7 @@ func (PdbAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { "analyzer_name": kind, }) - list, err := a.Client.GetClient().PolicyV1().PodDisruptionBudgets(a.Namespace).List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().PolicyV1().PodDisruptionBudgets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/pdb_test.go b/pkg/analyzer/pdb_test.go index a530494e13..dfd3a4bd82 100644 --- a/pkg/analyzer/pdb_test.go +++ b/pkg/analyzer/pdb_test.go @@ -115,3 +115,94 @@ func TestPodDisruptionBudgetAnalyzer(t *testing.T) { require.Equal(t, 1, len(results)) require.Equal(t, "test/PDB3", results[0].Name) } + +func TestPodDisruptionBudgetAnalyzerLabelSelectorFiltering(t *testing.T) { + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: fake.NewSimpleClientset( + &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "PDB1", + Namespace: "default", + Labels: map[string]string{ + "app": "pdb", + }, + }, + // Status conditions are nil. + Status: policyv1.PodDisruptionBudgetStatus{ + Conditions: []metav1.Condition{ + { + Type: "DisruptionAllowed", + Status: "False", + Reason: "test reason", + }, + }, + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: 0, + IntVal: 17, + StrVal: "17", + }, + MinAvailable: &intstr.IntOrString{ + Type: 0, + IntVal: 7, + StrVal: "7", + }, + // MatchLabels specified. + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label1": "test1", + "label2": "test2", + }, + }, + }, + }, + &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "PDB2", + Namespace: "default", + }, + // Status conditions are empty. + Status: policyv1.PodDisruptionBudgetStatus{ + Conditions: []metav1.Condition{ + { + Type: "DisruptionAllowed", + Status: "False", + Reason: "test reason", + }, + }, + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: 0, + IntVal: 17, + StrVal: "17", + }, + MinAvailable: &intstr.IntOrString{ + Type: 0, + IntVal: 7, + StrVal: "7", + }, + // MatchLabels specified. + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "label1": "test1", + "label2": "test2", + }, + }, + }, + }, + ), + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=pdb", + } + + pdbAnalyzer := PdbAnalyzer{} + results, err := pdbAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "default/PDB1", results[0].Name) +} diff --git a/pkg/analyzer/pod.go b/pkg/analyzer/pod.go index 10e9150fa4..8bc037d6ae 100644 --- a/pkg/analyzer/pod.go +++ b/pkg/analyzer/pod.go @@ -34,7 +34,9 @@ func (PodAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { }) // search all namespaces for pods that are not running - list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(a.Context, metav1.ListOptions{ + LabelSelector: a.LabelSelector, + }) if err != nil { return nil, err } diff --git a/pkg/analyzer/pvc.go b/pkg/analyzer/pvc.go index 699ef38ce3..1a4711d0f8 100644 --- a/pkg/analyzer/pvc.go +++ b/pkg/analyzer/pvc.go @@ -33,7 +33,7 @@ func (PvcAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { }) // search all namespaces for pods that are not running - list, err := a.Client.GetClient().CoreV1().PersistentVolumeClaims(a.Namespace).List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().CoreV1().PersistentVolumeClaims(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/pvc_test.go b/pkg/analyzer/pvc_test.go index 7e91aa1ffe..a2dcf8acb4 100644 --- a/pkg/analyzer/pvc_test.go +++ b/pkg/analyzer/pvc_test.go @@ -228,3 +228,53 @@ func TestPersistentVolumeClaimAnalyzer(t *testing.T) { }) } } + +func TestPvcAnalyzerLabelSelectorFiltering(t *testing.T) { + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: fake.NewSimpleClientset( + &appsv1.Event{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Event1", + Namespace: "default", + }, + LastTimestamp: metav1.Time{ + Time: time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC), + }, + Reason: "ProvisioningFailed", + Message: "PVC Event1 provisioning failed", + }, + &appsv1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "PVC1", + Namespace: "default", + Labels: map[string]string{ + "app": "pvc", + }, + }, + Status: appsv1.PersistentVolumeClaimStatus{ + Phase: appsv1.ClaimPending, + }, + }, + &appsv1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "PVC2", + Namespace: "default", + }, + Status: appsv1.PersistentVolumeClaimStatus{ + Phase: appsv1.ClaimPending, + }, + }, + ), + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=pvc", + } + + pvcAnalyzer := PvcAnalyzer{} + results, err := pvcAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "default/PVC1", results[0].Name) +} diff --git a/pkg/analyzer/rs.go b/pkg/analyzer/rs.go index be3fa493fc..66f44cd01e 100644 --- a/pkg/analyzer/rs.go +++ b/pkg/analyzer/rs.go @@ -32,7 +32,7 @@ func (ReplicaSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { }) // search all namespaces for pods that are not running - list, err := a.Client.GetClient().AppsV1().ReplicaSets(a.Namespace).List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().AppsV1().ReplicaSets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/rs_test.go b/pkg/analyzer/rs_test.go index 11eb871d7e..1c72fd6320 100644 --- a/pkg/analyzer/rs_test.go +++ b/pkg/analyzer/rs_test.go @@ -144,3 +144,58 @@ func TestReplicaSetAnalyzer(t *testing.T) { require.Equal(t, expectations[i].failuresCount, len(result.Error)) } } + +func TestReplicaSetAnalyzerLabelSelectorFiltering(t *testing.T) { + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: fake.NewSimpleClientset( + &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ReplicaSet1", + Namespace: "default", + Labels: map[string]string{ + "app": "replicaset", + }, + }, + Status: appsv1.ReplicaSetStatus{ + Replicas: 0, + Conditions: []appsv1.ReplicaSetCondition{ + { + // Should contribute to failures. + Type: appsv1.ReplicaSetReplicaFailure, + Reason: "FailedCreate", + Message: "failed to create test replica set 1", + }, + }, + }, + }, + &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ReplicaSet2", + Namespace: "default", + }, + Status: appsv1.ReplicaSetStatus{ + Replicas: 0, + Conditions: []appsv1.ReplicaSetCondition{ + { + // Should contribute to failures. + Type: appsv1.ReplicaSetReplicaFailure, + Reason: "FailedCreate", + Message: "failed to create test replica set 1", + }, + }, + }, + }, + ), + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=replicaset", + } + + rsAnalyzer := ReplicaSetAnalyzer{} + results, err := rsAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "default/ReplicaSet1", results[0].Name) +} diff --git a/pkg/analyzer/service.go b/pkg/analyzer/service.go index 47d64634e1..0c18d71ddf 100644 --- a/pkg/analyzer/service.go +++ b/pkg/analyzer/service.go @@ -45,7 +45,7 @@ func (ServiceAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { }) // search all namespaces for pods that are not running - list, err := a.Client.GetClient().CoreV1().Endpoints(a.Namespace).List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().CoreV1().Endpoints(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/service_test.go b/pkg/analyzer/service_test.go index d2abdca6f6..02b7f8745d 100644 --- a/pkg/analyzer/service_test.go +++ b/pkg/analyzer/service_test.go @@ -165,3 +165,106 @@ func TestServiceAnalyzer(t *testing.T) { require.Equal(t, expectations[i].failuresCount, len(result.Error)) } } + +func TestServiceAnalyzerLabelSelectorFiltering(t *testing.T) { + clientSet := + fake.NewSimpleClientset( + &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Endpoint1", + Namespace: "default", + Labels: map[string]string{ + "app": "service", + "part-of": "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.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Service1", + Namespace: "default", + Labels: map[string]string{ + "app": "service", + }, + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app1": "test-app1", + "app2": "test-app2", + }, + }, + }, + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "Service2", + Namespace: "default", + }, + Spec: v1.ServiceSpec{ + Selector: map[string]string{ + "app1": "test-app1", + "app2": "test-app2", + }, + }, + }, + ) + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientSet, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=service", + } + + sAnalyzer := ServiceAnalyzer{} + results, err := sAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "default/Endpoint1", results[0].Name) + + config = common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientSet, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=service,part-of=test", + } + + sAnalyzer = ServiceAnalyzer{} + results, err = sAnalyzer.Analyze(config) + require.NoError(t, err) + require.Equal(t, 1, len(results)) + require.Equal(t, "default/Endpoint1", results[0].Name) +} diff --git a/pkg/analyzer/statefulset.go b/pkg/analyzer/statefulset.go index fbe9c605a8..64b5d8b307 100644 --- a/pkg/analyzer/statefulset.go +++ b/pkg/analyzer/statefulset.go @@ -41,7 +41,7 @@ func (StatefulSetAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { "analyzer_name": kind, }) - list, err := a.Client.GetClient().AppsV1().StatefulSets(a.Namespace).List(a.Context, metav1.ListOptions{}) + list, err := a.Client.GetClient().AppsV1().StatefulSets(a.Namespace).List(a.Context, metav1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/statefulset_test.go b/pkg/analyzer/statefulset_test.go index d4b04aeae5..6aa8dd8e23 100644 --- a/pkg/analyzer/statefulset_test.go +++ b/pkg/analyzer/statefulset_test.go @@ -188,3 +188,55 @@ func TestStatefulSetAnalyzerNamespaceFiltering(t *testing.T) { } assert.Equal(t, len(analysisResults), 1) } + +func TestStatefulSetAnalyzerLabelSelectorFiltering(t *testing.T) { + clientSet := fake.NewSimpleClientset( + &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example1", + Namespace: "default", + Labels: map[string]string{ + "app": "statefulset", + "part-of": "test", + }, + }, + }, + &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example2", + Namespace: "default", + }, + }, + ) + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientSet, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=statefulset", + } + statefulSetAnalyzer := StatefulSetAnalyzer{} + results, err := statefulSetAnalyzer.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, 1, len(results)) + assert.Equal(t, "default/example1", results[0].Name) + + config = common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientSet, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=statefulset,part-of=test", + } + statefulSetAnalyzer = StatefulSetAnalyzer{} + results, err = statefulSetAnalyzer.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, 1, len(results)) + assert.Equal(t, "default/example1", results[0].Name) +} diff --git a/pkg/analyzer/validating_webhook.go b/pkg/analyzer/validating_webhook.go index cf97daaccd..fcd8bbfb0a 100644 --- a/pkg/analyzer/validating_webhook.go +++ b/pkg/analyzer/validating_webhook.go @@ -42,7 +42,7 @@ func (ValidatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, er "analyzer_name": kind, }) - validatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.Background(), v1.ListOptions{}) + validatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.Background(), v1.ListOptions{LabelSelector: a.LabelSelector}) if err != nil { return nil, err } diff --git a/pkg/analyzer/validating_webhook_test.go b/pkg/analyzer/validating_webhook_test.go index 6b7260d5b7..5665792fce 100644 --- a/pkg/analyzer/validating_webhook_test.go +++ b/pkg/analyzer/validating_webhook_test.go @@ -138,3 +138,80 @@ func TestValidatingWebhookAnalyzer(t *testing.T) { resultsLen := 3 require.Equal(t, resultsLen, len(results)) } + +func TestValidatingWebhookAnalyzerLabelSelectorFiltering(t *testing.T) { + clientSet := fake.NewSimpleClientset( + &admissionregistrationv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-validating-webhook-config1", + Namespace: "default", + Labels: map[string]string{ + "app": "validating-webhook", + "part-of": "test", + }, + }, + Webhooks: []admissionregistrationv1.ValidatingWebhook{ + { + // Failure: Pointing to an inactive receiver pod + Name: "webhook1", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: "test-service1", + Namespace: "default", + }, + }, + }, + }, + }, + &admissionregistrationv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-validating-webhook-config2", + Namespace: "default", + }, + Webhooks: []admissionregistrationv1.ValidatingWebhook{ + { + // Failure: Pointing to an inactive receiver pod + Name: "webhook1", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + Service: &admissionregistrationv1.ServiceReference{ + Name: "test-service1", + Namespace: "default", + }, + }, + }, + }, + }, + ) + + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientSet, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=validating-webhook", + } + + vwAnalyzer := ValidatingWebhookAnalyzer{} + results, err := vwAnalyzer.Analyze(config) + if err != nil { + t.Error(err) + } + require.Equal(t, 1, len(results)) + + config = common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientSet, + }, + Context: context.Background(), + Namespace: "default", + LabelSelector: "app=validating-webhook,part-of=test", + } + + vwAnalyzer = ValidatingWebhookAnalyzer{} + results, err = vwAnalyzer.Analyze(config) + if err != nil { + t.Error(err) + } + require.Equal(t, 1, len(results)) +} diff --git a/pkg/common/types.go b/pkg/common/types.go index 4e9d10cac6..9a3341804f 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -39,6 +39,7 @@ type Analyzer struct { Client *kubernetes.Client Context context.Context Namespace string + LabelSelector string AIClient ai.IAI PreAnalysis map[string]PreAnalysis Results []Result diff --git a/pkg/server/analyze.go b/pkg/server/analyze.go index 2be0b3211b..4362f8d411 100644 --- a/pkg/server/analyze.go +++ b/pkg/server/analyze.go @@ -25,6 +25,7 @@ func (h *handler) Analyze(ctx context.Context, i *schemav1.AnalyzeRequest) ( i.Language, i.Filters, i.Namespace, + i.LabelSelector, i.Nocache, i.Explain, int(i.MaxConcurrency), diff --git a/pkg/util/util.go b/pkg/util/util.go index 4babf28a57..af04faeee7 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -26,6 +26,8 @@ import ( "regexp" "strings" + "k8s.io/apimachinery/pkg/labels" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -295,3 +297,17 @@ func NewHeaders(customHeaders []string) []http.Header { return result } + +func LabelStrToSelector(labelStr string) labels.Selector { + if labelStr == "" { + return nil + } + labelSelectorMap := make(map[string]string) + for _, s := range strings.Split(labelStr, ",") { + parts := strings.SplitN(s, "=", 2) + if len(parts) == 2 { + labelSelectorMap[parts[0]] = parts[1] + } + } + return labels.SelectorFromSet(labels.Set(labelSelectorMap)) +}