Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions charts/kafka-operator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The command removes all the Kubernetes components associated with the chart and
| webhook.certs.secret | string | `"kafka-operator-serving-cert"` | Helm chart will use the secret name applied here for the cert |
| certManager.enabled | bool | `false` | Operator will integrate with the cert manager |
| certManager.namespace | string | `"cert-manager"` | Operator will look for the cert manager in this namespace namespace field specifies the Cert-manager's Cluster Resource Namespace. https://cert-manager.io/docs/configuration/ |
| contour.enabled | bool | `false` | Enable Project Contour ingress integration. Only enable this when the Kafka cluster uses `ingressController: contour`. When enabled, Project Contour's HTTPProxy CRD (projectcontour.io/v1) must be installed in the cluster, otherwise the operator fails to start. https://projectcontour.io |
| certSigning.enabled | bool | `true` | Enable native certificate signing integration |
| alertManager.enable | bool | `true` | AlertManager can be enabled |
| alertManager.port | int | `9001` | AlertManager port |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ spec:
- --enable-leader-election
- --cert-manager-namespace={{ .Values.certManager.namespace }}
- --cert-manager-enabled={{ .Values.certManager.enabled }}
- --contour-enabled={{ .Values.contour.enabled }}
{{- if not .Values.certSigning.enabled }}
- --disable-cert-signing-support
{{- end }}
Expand Down
8 changes: 8 additions & 0 deletions charts/kafka-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ certManager:
# https://cert-manager.io/docs/configuration/
namespace: "cert-manager"

contour:
# -- Enable Project Contour ingress integration. Only enable this when the
# Kafka cluster uses `ingressController: contour`. When enabled, Project
# Contour's HTTPProxy CRD (projectcontour.io/v1) must be installed in the
# cluster, otherwise the operator fails to start.
# https://projectcontour.io
enabled: false

certSigning:
# -- Enable native certificate signing integration
enabled: true
Expand Down
11 changes: 9 additions & 2 deletions controllers/kafkacluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ func (r *KafkaClusterReconciler) updateAndFetchLatest(ctx context.Context, clust
}

// SetupKafkaClusterWithManager registers kafka cluster controller to the manager
func SetupKafkaClusterWithManager(mgr ctrl.Manager) *ctrl.Builder {
func SetupKafkaClusterWithManager(mgr ctrl.Manager, contourEnabled bool) *ctrl.Builder {
log := mgr.GetLogger()
builder := ctrl.NewControllerManagedBy(mgr).
For(&v1beta1.KafkaCluster{}).
Expand All @@ -369,7 +369,14 @@ func SetupKafkaClusterWithManager(mgr ctrl.Manager) *ctrl.Builder {

kafkaWatches(builder)
envoyWatches(builder)
contourWatches(builder)
// The Contour watch (Owns(&contour.HTTPProxy{})) is only registered when
// Contour ingress is enabled. Registering it unconditionally makes the
// manager depend on Project Contour's HTTPProxy CRD being installed: when it
// is absent the informer never syncs and the operator crash-loops.
// See https://github.com/adobe/koperator/issues/229.
if contourEnabled {
contourWatches(builder)
}
cruiseControlWatches(builder)

builder.WithEventFilter(
Expand Down
2 changes: 1 addition & 1 deletion controllers/tests/clusterregistry/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ var _ = BeforeSuite(func() {
Expect(mgr).ToNot(BeNil())

kafkaClusterReconciler = NewTestReconciler()
err = controllers.SetupKafkaClusterWithManager(mgr).Named("KafkaCluster").Complete(kafkaClusterReconciler)
err = controllers.SetupKafkaClusterWithManager(mgr, true).Named("KafkaCluster").Complete(kafkaClusterReconciler)
Expect(err).NotTo(HaveOccurred())

kafkaTopicReconciler = NewTestReconciler()
Expand Down
138 changes: 138 additions & 0 deletions controllers/tests/contourwatch/contour_watch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright 2026 Cisco Systems, Inc. and/or its affiliates
// Copyright 2026 Adobe. All rights reserved.
//
// 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 contourwatch contains a focused regression test for
// https://github.com/adobe/koperator/issues/229: the operator must not
// require Project Contour's HTTPProxy CRD unless Contour ingress is enabled.
//
// The KafkaCluster controller Owns(&contour.HTTPProxy{}); if that watch is
// registered while the CRD is absent, controller-runtime never syncs the
// informer, mgr.Start returns an error and the operator pod CrashLoopBackOffs.
// This test boots an envtest API server WITHOUT the Contour CRD and asserts
// the manager stays healthy when Contour is disabled.
package contourwatch

import (
"context"
"errors"
"path/filepath"
"testing"
"time"

"k8s.io/apimachinery/pkg/runtime"
k8sscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/config"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/metrics/server"

contour "github.com/projectcontour/contour/apis/projectcontour/v1"

banzaicloudv1beta1 "github.com/banzaicloud/koperator/api/v1beta1"
"github.com/banzaicloud/koperator/controllers"
"github.com/banzaicloud/koperator/pkg/kafkaclient"
)

const (
// cacheSyncTimeout bounds how long a controller waits for its informers to
// sync. When the Contour watch is (incorrectly) active without the CRD, the
// manager reports the sync failure after this timeout.
cacheSyncTimeout = 15 * time.Second
// healthyGrace must exceed cacheSyncTimeout: if the manager survives this
// long without exiting, its caches synced, meaning the HTTPProxy watch was
// not registered.
healthyGrace = 20 * time.Second
)

// TestContourDisabledManagerStartsWithoutContourCRD reproduces issue #229: with
// Contour disabled and the projectcontour HTTPProxy CRD absent, the manager must
// start and keep running instead of crash-looping on a never-syncing informer.
func TestContourDisabledManagerStartsWithoutContourCRD(t *testing.T) {
testEnv := &envtest.Environment{
ErrorIfCRDPathMissing: true,
// Deliberately install only Koperator's own CRDs — no projectcontour —
// to mirror a cluster that does not run Project Contour.
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "..", "config", "base", "crds"),
},
}

cfg, err := testEnv.Start()
if err != nil {
t.Fatalf("failed to start envtest control plane: %v", err)
}
t.Cleanup(func() {
if stopErr := testEnv.Stop(); stopErr != nil {
t.Logf("failed to stop envtest control plane: %v", stopErr)
}
})

scheme := runtime.NewScheme()
if err := k8sscheme.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add client-go scheme: %v", err)
}
if err := banzaicloudv1beta1.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add koperator scheme: %v", err)
}
// The Contour types are registered on the scheme exactly as in main.go, so
// the only thing that differs between the crash-looping and fixed behavior
// is whether the HTTPProxy watch is wired up.
if err := contour.AddToScheme(scheme); err != nil {
t.Fatalf("failed to add contour scheme: %v", err)
}

mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme,
Metrics: server.Options{BindAddress: "0"},
LeaderElection: false,
Controller: config.Controller{CacheSyncTimeout: cacheSyncTimeout},
})
if err != nil {
t.Fatalf("failed to create manager: %v", err)
}

reconciler := &controllers.KafkaClusterReconciler{
Client: mgr.GetClient(),
DirectClient: mgr.GetAPIReader(),
KafkaClientProvider: kafkaclient.NewMockProvider(),
}

const contourEnabled = false
if err := controllers.SetupKafkaClusterWithManager(mgr, contourEnabled).Complete(reconciler); err != nil {
t.Fatalf("failed to set up KafkaCluster controller: %v", err)
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

startErr := make(chan error, 1)
go func() {
startErr <- mgr.Start(ctx)
}()

select {
case err := <-startErr:
t.Fatalf("manager exited before becoming healthy (regression of issue #229 — "+
"the Contour HTTPProxy watch is active without the Contour CRD installed): %v", err)
case <-time.After(healthyGrace):
// Manager survived past the cache-sync timeout: informers synced, so the
// HTTPProxy watch was correctly skipped while Contour is disabled.
}

cancel()
if err := <-startErr; err != nil && !errors.Is(err, context.Canceled) {
t.Fatalf("manager Start returned an unexpected error on shutdown: %v", err)
}
}
2 changes: 1 addition & 1 deletion controllers/tests/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ var _ = BeforeSuite(func(ctx SpecContext) {
KafkaClientProvider: kafkaclient.NewMockProvider(),
}

err = controllers.SetupKafkaClusterWithManager(mgr).Complete(&kafkaClusterReconciler)
err = controllers.SetupKafkaClusterWithManager(mgr, true).Complete(&kafkaClusterReconciler)
Expect(err).NotTo(HaveOccurred())

kafkaTopicReconciler := &controllers.KafkaTopicReconciler{
Expand Down
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func main() {
verboseLogging bool
certSigningDisabled bool
certManagerEnabled bool
contourEnabled bool
maxKafkaTopicConcurrentReconciles int
healthProbesAddr string
)
Expand All @@ -103,6 +104,7 @@ func main() {
flag.BoolVar(&developmentLogging, "development", false, "Enable development logging")
flag.BoolVar(&verboseLogging, "verbose", false, "Enable verbose logging")
flag.BoolVar(&certManagerEnabled, "cert-manager-enabled", false, "Enable cert-manager integration")
flag.BoolVar(&contourEnabled, "contour-enabled", false, "Enable Project Contour ingress integration. Requires Contour's HTTPProxy CRD to be installed in the cluster.")
flag.BoolVar(&certSigningDisabled, "disable-cert-signing-support", false, "Disable native certificate signing integration")
flag.IntVar(&maxKafkaTopicConcurrentReconciles, "max-kafka-topic-concurrent-reconciles", 10, "Define max amount of concurrent KafkaTopic reconciles")
flag.StringVar(&healthProbesAddr, "health-probes-addr", ":8081", "The address the probe endpoint binds to.")
Expand Down Expand Up @@ -180,7 +182,7 @@ func main() {
KafkaClientProvider: kafkaclient.NewDefaultProvider(),
}

if err = controllers.SetupKafkaClusterWithManager(mgr).Complete(kafkaClusterReconciler); err != nil {
if err = controllers.SetupKafkaClusterWithManager(mgr, contourEnabled).Complete(kafkaClusterReconciler); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "KafkaCluster")
os.Exit(1)
}
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const (

// Common string values and CLI flags/keys.
falseString = "false"
trueString = "true"
timeoutFlag = "--timeout"
timeoutValue = "10m"
outputFlag = "--output"
Expand Down
11 changes: 7 additions & 4 deletions tests/e2e/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ var (
"crds/kafkatopics.yaml",
"crds/kafkausers.yaml",
},
// Contour is installed as a dependency above (with its HTTPProxy
// CRD), so enable the operator's Contour integration to exercise it.
SetValues: map[string]string{
"contour.enabled": trueString,
},
}
// Set helm chart values for Koperator to be able to use custom image
koperatorImagePath := os.Getenv("IMG_E2E")
Expand All @@ -83,10 +88,8 @@ var (
koperatorImageTag = koperatorImagePathSplit[1]
}

koperatorLocalHelmDescriptor.SetValues = map[string]string{
"operator.image.repository": koperatorImageRepository,
"operator.image.tag": koperatorImageTag,
}
koperatorLocalHelmDescriptor.SetValues["operator.image.repository"] = koperatorImageRepository
koperatorLocalHelmDescriptor.SetValues["operator.image.tag"] = koperatorImageTag
}

return koperatorLocalHelmDescriptor
Expand Down