diff --git a/charts/kagenti-operator/templates/rbac/role.yaml b/charts/kagenti-operator/templates/rbac/role.yaml index c4445640..cbb8e9a1 100755 --- a/charts/kagenti-operator/templates/rbac/role.yaml +++ b/charts/kagenti-operator/templates/rbac/role.yaml @@ -181,6 +181,7 @@ rules: - apiGroups: - operator.openshift.io resources: + - networks - zerotrustworkloadidentitymanagers verbs: - get diff --git a/kagenti-operator/cmd/main.go b/kagenti-operator/cmd/main.go index 4a321e6d..9dcace66 100644 --- a/kagenti-operator/cmd/main.go +++ b/kagenti-operator/cmd/main.go @@ -21,6 +21,7 @@ import ( "crypto/tls" "errors" "flag" + "fmt" "os" "path/filepath" "strings" @@ -482,6 +483,14 @@ func main() { os.Exit(1) } + if controller.NetworkOperatorCRDExists(mgr.GetConfig()) { + if warning := controller.CheckOVNNetworkConfig(context.Background(), mgr.GetAPIReader()); warning != "" { + setupLog.Error(fmt.Errorf("OVN network misconfiguration"), warning) + } else { + setupLog.Info("OVN-Kubernetes routingViaHost is correctly configured") + } + } + artReconciler := &controller.AgentRuntimeReconciler{ Client: mgr.GetClient(), APIReader: mgr.GetAPIReader(), diff --git a/kagenti-operator/config/rbac/role.yaml b/kagenti-operator/config/rbac/role.yaml index 6c25be22..4c365137 100644 --- a/kagenti-operator/config/rbac/role.yaml +++ b/kagenti-operator/config/rbac/role.yaml @@ -179,6 +179,7 @@ rules: - apiGroups: - operator.openshift.io resources: + - networks - zerotrustworkloadidentitymanagers verbs: - get diff --git a/kagenti-operator/internal/controller/agentruntime_controller.go b/kagenti-operator/internal/controller/agentruntime_controller.go index f264d7d3..524e207c 100644 --- a/kagenti-operator/internal/controller/agentruntime_controller.go +++ b/kagenti-operator/internal/controller/agentruntime_controller.go @@ -124,6 +124,7 @@ func (r *AgentRuntimeReconciler) getFeatureGates() *webhookconfig.FeatureGates { // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch // +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=operator.openshift.io,resources=networks,verbs=get func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) diff --git a/kagenti-operator/internal/controller/network_check.go b/kagenti-operator/internal/controller/network_check.go new file mode 100644 index 00000000..35e7534c --- /dev/null +++ b/kagenti-operator/internal/controller/network_check.go @@ -0,0 +1,90 @@ +/* +Copyright 2026. + +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 controller + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var networkOperatorGVK = schema.GroupVersionKind{ + Group: "operator.openshift.io", + Version: "v1", + Kind: "Network", +} + +// NetworkOperatorCRDExists checks whether the network.operator.openshift.io CRD +// is available on the cluster. Used at startup to conditionally run the network +// check (same pattern as TektonConfigCRDExists). +func NetworkOperatorCRDExists(cfg *rest.Config) bool { + dc, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return false + } + resources, err := dc.ServerResourcesForGroupVersion("operator.openshift.io/v1") + if err != nil { + return false + } + for _, r := range resources.APIResources { + if r.Kind == "Network" { + return true + } + } + return false +} + +// CheckOVNNetworkConfig reads network.operator.openshift.io/cluster and returns +// a warning message if OVN-Kubernetes is detected without routingViaHost enabled. +// Returns an empty string when the configuration is correct or not applicable. +func CheckOVNNetworkConfig(ctx context.Context, c client.Reader) string { + network := &unstructured.Unstructured{} + network.SetGroupVersionKind(networkOperatorGVK) + + if err := c.Get(ctx, types.NamespacedName{Name: "cluster"}, network); err != nil { + return fmt.Sprintf("could not read network.operator.openshift.io/cluster: %v", err) + } + + networkType, found, err := unstructured.NestedString(network.Object, "spec", "defaultNetwork", "type") + if err != nil || !found { + return "" + } + + if networkType != "OVNKubernetes" { + return "" + } + + routingViaHost, found, err := unstructured.NestedBool( + network.Object, + "spec", "defaultNetwork", "ovnKubernetesConfig", "gatewayConfig", "routingViaHost", + ) + if err != nil || !found || !routingViaHost { + return "OVN-Kubernetes detected but routingViaHost is not enabled — " + + "Istio ambient mTLS will not function correctly. " + + "Apply the routingViaHost patch: kubectl patch network.operator.openshift.io cluster " + + "--type=merge -p '{\"spec\":{\"defaultNetwork\":{\"ovnKubernetesConfig\":" + + "{\"gatewayConfig\":{\"routingViaHost\":true}}}}}'" + } + + return "" +} diff --git a/kagenti-operator/internal/controller/network_check_test.go b/kagenti-operator/internal/controller/network_check_test.go new file mode 100644 index 00000000..19f048e2 --- /dev/null +++ b/kagenti-operator/internal/controller/network_check_test.go @@ -0,0 +1,113 @@ +/* +Copyright 2026. + +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 controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func newNetworkResource(networkType string, routingViaHost *bool) *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "operator.openshift.io/v1", + "kind": "Network", + "metadata": map[string]interface{}{ + "name": "cluster", + }, + "spec": map[string]interface{}{ + "defaultNetwork": map[string]interface{}{ + "type": networkType, + }, + }, + }, + } + if routingViaHost != nil { + _ = unstructured.SetNestedField(obj.Object, *routingViaHost, + "spec", "defaultNetwork", "ovnKubernetesConfig", "gatewayConfig", "routingViaHost") + } + return obj +} + +var _ = Describe("CheckOVNNetworkConfig", func() { + ctx := context.Background() + + It("should return warning when OVN routingViaHost is not set", func() { + network := newNetworkResource("OVNKubernetes", nil) + fc := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(network). + Build() + + warning := CheckOVNNetworkConfig(ctx, fc) + + Expect(warning).To(ContainSubstring("routingViaHost is not enabled")) + }) + + It("should return warning when routingViaHost is explicitly false", func() { + rvh := false + network := newNetworkResource("OVNKubernetes", &rvh) + fc := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(network). + Build() + + warning := CheckOVNNetworkConfig(ctx, fc) + + Expect(warning).To(ContainSubstring("routingViaHost is not enabled")) + }) + + It("should return empty string when routingViaHost is true", func() { + rvh := true + network := newNetworkResource("OVNKubernetes", &rvh) + fc := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(network). + Build() + + warning := CheckOVNNetworkConfig(ctx, fc) + + Expect(warning).To(BeEmpty()) + }) + + It("should return empty string for non-OVN network types", func() { + network := newNetworkResource("OpenShiftSDN", nil) + fc := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(network). + Build() + + warning := CheckOVNNetworkConfig(ctx, fc) + + Expect(warning).To(BeEmpty()) + }) + + It("should return error message when network resource is missing", func() { + fc := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + Build() + + warning := CheckOVNNetworkConfig(ctx, fc) + + Expect(warning).To(ContainSubstring("could not read")) + }) +})