Skip to content

Commit 5bc1a26

Browse files
authored
Harden Istio injection check (#7)
- Update istio injection check to validate against mutating webhooks instead of solely on labels themselves. This is similar to the `istioctl x check-inject` method. - Add `--max-processors` flag to limit the number of parallel processes during namespace/pod and node processing.
1 parent 32e2ece commit 5bc1a26

26 files changed

+1739
-150
lines changed

.github/workflows/pull-request.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
e2e-tests:
3838
name: Run E2E Tests
3939
runs-on: ubuntu-24.04
40-
timeout-minutes: 10 # As of 04-11-2025, tests usually complete around 2 minute, so 10 minutes is a safe timeout.
40+
timeout-minutes: 10 # As of 04-30-2025, tests usually complete in around 3 minutes, so 10 minutes is a safe timeout.
4141
steps:
4242
- uses: actions/checkout@v4
4343
with:

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ run-unit-tests:
8383
run-e2e-tests:
8484
@gotestsum --junitfile junit-e2e-test.xml -- -tags=e2e ./...
8585

86+
run-performance-tests:
87+
@gotestsum --junitfile junit-performance-test.xml -- -tags=performance ./...
88+
8689
add-test-dependencies:
8790
@helm repo add metrics-server "https://kubernetes-sigs.github.io/metrics-server/"
8891
@helm repo add istio "https://istio-release.storage.googleapis.com/charts"

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ The Istio Usage Collector is a Go implementation of the [`gather-cluster-info.sh
1414

1515
This data is collected into a JSON or YAML file that can be used for further analysis through our detailed migration estimator tool.
1616

17+
Istio data gathered and reported is based on automatic sidecar injection defined within Istio's MutatingAdmissionWebhooks.
18+
1719
## Installation
1820

1921
### Downloading release

changelogs/v0.1.5.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
new-feature:
2+
- Add a `max-processors` flag to the command to limit the number of processors used.
3+
4+
enhancement:
5+
- Use Istio's MutatingAdmissionWebhooks to check for automatic istio injection instead of manual label checks. This is to simulate Istio's `istioctl x check-inject` command.
6+
7+
other:
8+
- Add performance tests for the namespace processor.

cmd/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type CommandFlags struct {
2424
OutputFilePrefix string
2525
EnableDebug bool
2626
NoProgress bool
27+
MaxProcessors int
2728
}
2829

2930
// DefaultFlags returns a CommandFlags struct initialized with default values
@@ -37,6 +38,7 @@ func DefaultFlags() *CommandFlags {
3738
OutputFilePrefix: "",
3839
EnableDebug: false,
3940
NoProgress: false,
41+
MaxProcessors: 0,
4042
}
4143
}
4244

@@ -135,6 +137,7 @@ func GetCommand(customFlags ...*CommandFlags) *cobra.Command {
135137
OutputFormat: flags.OutputFormat,
136138
OutputFilePrefix: prefix,
137139
NoProgress: flags.NoProgress,
140+
MaxProcessors: flags.MaxProcessors,
138141
}
139142

140143
// Gather cluster information
@@ -157,6 +160,7 @@ func GetCommand(customFlags ...*CommandFlags) *cobra.Command {
157160
cmd.PersistentFlags().StringVarP(&flags.OutputFilePrefix, "output-prefix", "p", "", "Custom prefix for the output file. If not set, uses the cluster name.")
158161
cmd.PersistentFlags().BoolVar(&flags.EnableDebug, "debug", false, "Enable debug mode.")
159162
cmd.PersistentFlags().BoolVar(&flags.NoProgress, "no-progress", false, "Disable the progress bar while processing resources.")
163+
cmd.PersistentFlags().IntVar(&flags.MaxProcessors, "max-processors", 0, "Maximum number of processors to use. If not set, or <= 0, it will use all available processors.")
160164

161165
return cmd
162166
}

cmd/root_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func TestRootCommandFlags(t *testing.T) {
1919
assert.NotNil(t, cmd.Flag("output-prefix"))
2020
assert.NotNil(t, cmd.Flag("debug"))
2121
assert.NotNil(t, cmd.Flag("no-progress"))
22+
assert.NotNil(t, cmd.Flag("max-processors"))
2223

2324
assert.Nil(t, cmd.Flag("version")) // This is only set for builds in standalone mode, not part of the command in general
2425
}
@@ -45,6 +46,7 @@ func TestRootCommandFlagParsing(t *testing.T) {
4546
OutputFilePrefix: "", // Default is empty string, where during execution, the context name is used
4647
EnableDebug: false,
4748
NoProgress: false,
49+
MaxProcessors: 0,
4850
},
4951
},
5052
{
@@ -58,6 +60,7 @@ func TestRootCommandFlagParsing(t *testing.T) {
5860
"--output-prefix", "my-prefix",
5961
"--debug",
6062
"--no-progress",
63+
"--max-processors", "1",
6164
},
6265
expectedFlags: CommandFlags{
6366
HideNames: true,
@@ -68,6 +71,7 @@ func TestRootCommandFlagParsing(t *testing.T) {
6871
OutputFilePrefix: "my-prefix",
6972
EnableDebug: true,
7073
NoProgress: true,
74+
MaxProcessors: 1,
7175
},
7276
},
7377
{
@@ -89,6 +93,7 @@ func TestRootCommandFlagParsing(t *testing.T) {
8993
OutputFilePrefix: "short-p",
9094
EnableDebug: false, // default
9195
NoProgress: false, // default
96+
MaxProcessors: 0, // default
9297
},
9398
},
9499
}
@@ -134,6 +139,10 @@ func TestRootCommandFlagParsing(t *testing.T) {
134139
noProgress, err := cmdFlags.GetBool("no-progress")
135140
assert.NoError(t, err)
136141
assert.Equal(t, tt.expectedFlags.NoProgress, noProgress, "Flag NoProgress mismatch")
142+
143+
maxProcessors, err := cmdFlags.GetInt("max-processors")
144+
assert.NoError(t, err)
145+
assert.Equal(t, tt.expectedFlags.MaxProcessors, maxProcessors, "Flag MaxProcessors mismatch")
137146
})
138147
}
139148
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/solo-io/istio-usage-collector
33
go 1.23.5
44

55
require (
6+
github.com/google/go-cmp v0.6.0
67
github.com/pterm/pterm v0.12.80
78
github.com/spf13/cobra v1.9.1
89
github.com/stretchr/testify v1.10.0
@@ -11,6 +12,7 @@ require (
1112
k8s.io/apimachinery v0.32.3
1213
k8s.io/client-go v0.32.3
1314
k8s.io/metrics v0.32.3
15+
sigs.k8s.io/yaml v1.4.0
1416
)
1517

1618
require (
@@ -28,7 +30,6 @@ require (
2830
github.com/gogo/protobuf v1.3.2 // indirect
2931
github.com/golang/protobuf v1.5.4 // indirect
3032
github.com/google/gnostic-models v0.6.8 // indirect
31-
github.com/google/go-cmp v0.6.0 // indirect
3233
github.com/google/gofuzz v1.2.0 // indirect
3334
github.com/google/uuid v1.6.0 // indirect
3435
github.com/gookit/color v1.5.4 // indirect
@@ -61,5 +62,4 @@ require (
6162
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
6263
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
6364
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
64-
sigs.k8s.io/yaml v1.4.0 // indirect
6565
)

internal/gatherer/gatherer.go

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"sync"
1212
"time"
1313

14+
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
15+
1416
"github.com/solo-io/istio-usage-collector/internal/logging"
1517
"github.com/solo-io/istio-usage-collector/internal/utils"
1618
"github.com/solo-io/istio-usage-collector/pkg/models"
@@ -197,7 +199,12 @@ func processNodes(ctx context.Context, clientset kubernetes.Interface, metricsCl
197199
errorCh := make(chan error, totalNodes)
198200

199201
// Use a semaphore to control concurrency
200-
concurrentLimit := runtime.NumCPU() * 2
202+
var concurrentLimit int
203+
if cfg.MaxProcessors > 0 {
204+
concurrentLimit = cfg.MaxProcessors
205+
} else {
206+
concurrentLimit = runtime.NumCPU() * 2
207+
}
201208
semaphore := make(chan struct{}, concurrentLimit)
202209

203210
logging.Debug("Processing nodes with up to %d concurrent requests", concurrentLimit)
@@ -326,10 +333,30 @@ func processNamespaces(ctx context.Context, clientset kubernetes.Interface, metr
326333
errorCh := make(chan error, totalNamespaces)
327334

328335
// Use a semaphore to control concurrency based on system resources
329-
concurrentLimit := runtime.NumCPU() * 4
336+
var concurrentLimit int
337+
if cfg.MaxProcessors > 0 {
338+
concurrentLimit = cfg.MaxProcessors
339+
} else {
340+
concurrentLimit = runtime.NumCPU() * 4
341+
}
330342
semaphore := make(chan struct{}, concurrentLimit)
331343

332344
logging.Debug("Processing namespaces with up to %d concurrent requests", concurrentLimit)
345+
346+
// Get all mutating webhook configurations - istio uses mwhs to define its automatic sidecar injection policy
347+
webhooks, err := clientset.AdmissionregistrationV1().MutatingWebhookConfigurations().List(ctx, metav1.ListOptions{})
348+
if err != nil {
349+
logging.Warn("Failed to list mutating webhook configurations: %v", err)
350+
}
351+
if webhooks == nil || len(webhooks.Items) == 0 {
352+
logging.Warn("No mutating webhook configurations found in cluster %s", cfg.KubeContext)
353+
}
354+
// filter out non-istio webhooks
355+
istioWebhooks := utils.FilterIstioWebhooks(webhooks.Items)
356+
if len(istioWebhooks) == 0 {
357+
logging.Warn("No Istio-related mutating webhook configurations found in cluster %s", cfg.KubeContext)
358+
}
359+
333360
for _, ns := range namespaces.Items {
334361
// Check parent context for cancellation before spawning more goroutines
335362
if ctx.Err() != nil {
@@ -367,8 +394,7 @@ func processNamespaces(ctx context.Context, clientset kubernetes.Interface, metr
367394
return
368395
}
369396

370-
nsInfo, err := processNamespace(workerCtx, clientset, metricsClient, namespace.Name, hasMetrics)
371-
397+
nsInfo, err := processNamespace(workerCtx, clientset, metricsClient, namespace.Name, hasMetrics, istioWebhooks)
372398
if progress != nil {
373399
progress.Increment()
374400
}
@@ -410,8 +436,9 @@ func processNamespaces(ctx context.Context, clientset kubernetes.Interface, metr
410436
return nil
411437
}
412438

413-
// processNamespace processes an individual namespace
414-
func processNamespace(ctx context.Context, clientset kubernetes.Interface, metricsClient metricsv.Interface, namespace string, hasMetrics bool) (*models.NamespaceInfo, error) {
439+
// processNamespace processes an individual namespace and its pods
440+
// TODO: We currently don't check for Sidecar CRs -- https://istio.io/latest/docs/reference/config/networking/sidecar/
441+
func processNamespace(ctx context.Context, clientset kubernetes.Interface, metricsClient metricsv.Interface, namespace string, hasMetrics bool, istioWebhooks []admissionregistrationv1.MutatingWebhookConfiguration) (*models.NamespaceInfo, error) {
415442
if ctx.Err() != nil {
416443
return nil, ctx.Err()
417444
}
@@ -422,19 +449,6 @@ func processNamespace(ctx context.Context, clientset kubernetes.Interface, metri
422449
return nil, fmt.Errorf("failed to get namespace details: %w", err)
423450
}
424451

425-
isIstioInjected := false
426-
if value, ok := ns.Labels["istio-injection"]; ok && value == "enabled" {
427-
isIstioInjected = true
428-
} else if _, ok := ns.Labels["istio.io/rev"]; ok {
429-
isIstioInjected = true
430-
}
431-
432-
if isIstioInjected {
433-
logging.Debug("Namespace %s has Istio injection enabled", namespace)
434-
} else {
435-
logging.Debug("Namespace %s has no Istio injection", namespace)
436-
}
437-
438452
// Get pods in the namespace
439453
pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
440454
if err != nil {
@@ -478,11 +492,26 @@ func processNamespace(ctx context.Context, clientset kubernetes.Interface, metri
478492
istioCpuActual := 0.0
479493
istioMemActual := 0.0
480494

495+
// Whether the namespace has at least one pod with istio injection
496+
isIstioInjected := false
497+
481498
// Process all pods
482499
for _, pod := range pods.Items {
500+
// Check if istio injection is enabled on the pod-level
501+
isPodIstioInjected := utils.CheckInject(istioWebhooks, pod.Labels, ns.Labels)
502+
503+
// If any pod within the namespace has istio injection occurring, we should count the namespace as having istio injected
504+
isIstioInjected = isIstioInjected || isPodIstioInjected
505+
483506
// Check each container
484507
for _, container := range pod.Spec.Containers {
485-
isIstioProxy := container.Name == "istio-proxy"
508+
// we only count istio-proxy container as an istio sidecar if the pod has istio injection enabled
509+
isIstioProxyContainer := container.Name == "istio-proxy"
510+
isIstioProxy := isIstioProxyContainer && isPodIstioInjected
511+
if isIstioProxyContainer && !isPodIstioInjected {
512+
// add a debug log if the pod has an istio-proxy container but istio injection is disabled, meaning we won't treat it as an istio sidecar
513+
logging.Debug("%s.%s does not have istio injection enabled, treating its 'istio-proxy' container as a regular container", namespace, pod.Name)
514+
}
486515

487516
// Count container types
488517
if isIstioProxy {
@@ -539,9 +568,10 @@ func processNamespace(ctx context.Context, clientset kubernetes.Interface, metri
539568
}
540569
}
541570

542-
// Create namespace info
571+
// Create namespace info (before appending actual resource usage and istio resources)
543572
nsInfo := &models.NamespaceInfo{
544-
Pods: len(pods.Items),
573+
Pods: len(pods.Items),
574+
// the namespace had istio injected if it was either enabled on the namespace-level OR within any of its pods
545575
IsIstioInjected: isIstioInjected,
546576
Resources: models.ResourceInfo{
547577
Regular: models.ContainerResources{
@@ -561,8 +591,8 @@ func processNamespace(ctx context.Context, clientset kubernetes.Interface, metri
561591
}
562592
}
563593

564-
// Add Istio resources if the namespace has Istio injection
565-
if isIstioInjected {
594+
// Only add the Istio resources field if the namespace contained at least one pod with istio injection
595+
if nsInfo.IsIstioInjected {
566596
nsInfo.Resources.Istio = &models.ContainerResources{
567597
Containers: istioContainers,
568598
Request: models.Resources{
@@ -578,7 +608,6 @@ func processNamespace(ctx context.Context, clientset kubernetes.Interface, metri
578608
}
579609
}
580610
}
581-
582611
return nsInfo, nil
583612
}
584613

0 commit comments

Comments
 (0)