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
10 changes: 9 additions & 1 deletion kagenti-operator/config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ rules:
- ""
resources:
- namespaces
- services
verbs:
- get
- list
- patch
- watch
- apiGroups:
- ""
Expand All @@ -51,6 +51,14 @@ rules:
- create
- get
- update
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- agent.kagenti.dev
resources:
Expand Down
6 changes: 6 additions & 0 deletions kagenti-operator/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,13 @@ The AgentRuntime controller applies the following labels and annotations to the
| `Ready` | True | `Configured` | Labels and config-hash applied to the target workload |
| `Ready` | False | `ConfigHashError` | Failed to compute the config hash |
| `Ready` | False | `ConfigApplyError` | Failed to apply labels/annotations to the workload |
| `IstioMeshEnrolled` | True | `NamespaceLabeled` | Namespace labeled with `istio-discovery=enabled` and `istio.io/dataplane-mode=ambient` for Istio ambient mesh enrollment |
| `IstioMeshEnrolled` | False | `OptedOut` | Namespace has `kagenti.io/istio-mesh=disabled` annotation; Istio mesh labels not applied |
| `IstioMeshEnrolled` | False | `PatchFailed` | Failed to patch namespace labels (e.g., RBAC misconfiguration). Non-fatal; reconcile continues. |
| `SkillsDiscovered` | True | `SkillsFound` | Linked skills discovered from `kagenti.io/skills` annotation on the target workload |
| `SkillsMounted` | True | `SkillsApplied` | OCI skill ImageVolumes applied to the target workload |
| `SkillsMounted` | False | `FeatureGateDisabled` | Skills defined but `skillImageVolumes` feature gate is disabled |
| `SkillsMounted` | False | `UnsupportedWorkloadKind` | Skills defined but the target workload kind (e.g., Sandbox) does not support skill ImageVolumes |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: SkillsMounted condition docs (lines 483-485) appear unrelated to the Istio mesh enrollment feature — this condition isn't implemented anywhere in this diff. Consider moving to a separate commit or the PR that introduces the SkillsMounted logic, to keep this PR focused on its stated scope.


### Admission Validation

Expand Down
1 change: 1 addition & 0 deletions kagenti-operator/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ Source: `internal/controller/agentruntime_controller.go`
| `agent.kagenti.dev` | `agentruntimes/finalizers` | update | Add/remove `kagenti.io/cleanup` finalizer for graceful deletion |
| `apps` | `deployments`, `statefulsets` | get, list, watch, update, patch | Resolve targetRef, apply labels (`kagenti.io/type`, `managed-by`) and config-hash annotation |
| `""` (core) | `configmaps` | get, list, watch | Read cluster defaults (`kagenti-platform-config`), feature gates (`kagenti-feature-gates`), and namespace defaults |
| `""` (core) | `namespaces` | get, list, watch, patch | Read and label namespaces for Istio ambient mesh enrollment |
| `""` (core) | `pods` | get, list, watch | Count configured pods and verify ownership chains |
| `""` (core) | `events` | create, patch | Record reconciliation events (TargetNotFound, ConfigWarning, Configured) |

Expand Down
21 changes: 21 additions & 0 deletions kagenti-operator/internal/controller/agentruntime_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,27 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, req ctrl.Request
}
}

// 4.6. Ensure namespace has Istio ambient mesh labels for ztunnel mTLS.
istioLabeled, istioErr := r.ensureIstioMeshLabels(ctx, rt.Namespace)
switch {
case istioErr != nil:
logger.Error(istioErr, "Failed to ensure Istio mesh labels")
r.setCondition(rt, ConditionTypeIstioMeshEnrolled, metav1.ConditionFalse, "PatchFailed", istioErr.Error())
if r.Recorder != nil {
r.Recorder.Event(rt, corev1.EventTypeWarning, "IstioMeshLabelError", istioErr.Error())
}
case istioLabeled:
r.setCondition(rt, ConditionTypeIstioMeshEnrolled, metav1.ConditionTrue, "NamespaceLabeled",
fmt.Sprintf("Namespace %s enrolled in Istio ambient mesh", rt.Namespace))
if r.Recorder != nil {
r.Recorder.Event(rt, corev1.EventTypeNormal, "IstioMeshEnrolled",
fmt.Sprintf("Namespace %s labeled for Istio ambient mesh", rt.Namespace))
}
default:
r.setCondition(rt, ConditionTypeIstioMeshEnrolled, metav1.ConditionFalse, "OptedOut",
fmt.Sprintf("Namespace %s opted out of Istio mesh enrollment", rt.Namespace))
}

// 5. Compute config hash from merged configuration (cluster → namespace → CR)
configResult, err := ComputeConfigHash(ctx, r.Client, rt.Namespace, &rt.Spec)
if err != nil {
Expand Down
71 changes: 71 additions & 0 deletions kagenti-operator/internal/controller/agentruntime_istio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
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"

corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
ConditionTypeIstioMeshEnrolled = "IstioMeshEnrolled"

AnnotationIstioMeshOptOut = "kagenti.io/istio-mesh"

LabelIstioDiscovery = "istio-discovery"
LabelIstioDataplaneMode = "istio.io/dataplane-mode"
)

// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;patch

// ensureIstioMeshLabels patches the namespace with Istio ambient mesh labels
// unless the namespace has opted out via the kagenti.io/istio-mesh annotation.
// Returns true if labels were applied (or already present), false if opted out.
func (r *AgentRuntimeReconciler) ensureIstioMeshLabels(ctx context.Context, namespace string) (bool, error) {
logger := log.FromContext(ctx)

ns := &corev1.Namespace{}
if err := r.Get(ctx, client.ObjectKey{Name: namespace}, ns); err != nil {
return false, err
}

if ns.Annotations[AnnotationIstioMeshOptOut] == "disabled" {
logger.V(1).Info("Namespace opted out of Istio mesh enrollment", "namespace", namespace)
return false, nil
}

if ns.Labels[LabelIstioDiscovery] == "enabled" && ns.Labels[LabelIstioDataplaneMode] == "ambient" {
return true, nil
}

patch := client.MergeFrom(ns.DeepCopy())
if ns.Labels == nil {
ns.Labels = make(map[string]string)
}
ns.Labels[LabelIstioDiscovery] = "enabled"
ns.Labels[LabelIstioDataplaneMode] = "ambient"

if err := r.Patch(ctx, ns, patch); err != nil {
return false, err
}

logger.V(1).Info("Labeled namespace for Istio ambient mesh", "namespace", namespace)
return true, nil
}
131 changes: 131 additions & 0 deletions kagenti-operator/internal/controller/agentruntime_istio_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
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"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var _ = Describe("ensureIstioMeshLabels", func() {
var (
reconciler *AgentRuntimeReconciler
nsCounter int
)

newTestNamespace := func(labels map[string]string, annotations map[string]string) *corev1.Namespace {
nsCounter++
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("istio-test-%d", nsCounter),
Labels: labels,
Annotations: annotations,
},
}
Expect(k8sClient.Create(context.Background(), ns)).To(Succeed())
return ns
}

BeforeEach(func() {
reconciler = &AgentRuntimeReconciler{
Client: k8sClient,
}
})

It("should add Istio labels to a bare namespace", func() {
ns := newTestNamespace(nil, nil)

labeled, err := reconciler.ensureIstioMeshLabels(ctx, ns.Name)
Expect(err).NotTo(HaveOccurred())
Expect(labeled).To(BeTrue())

updated := &corev1.Namespace{}
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(ns), updated)).To(Succeed())
Expect(updated.Labels[LabelIstioDiscovery]).To(Equal("enabled"))
Expect(updated.Labels[LabelIstioDataplaneMode]).To(Equal("ambient"))
})

It("should be idempotent on already-labeled namespace", func() {
ns := newTestNamespace(map[string]string{
LabelIstioDiscovery: "enabled",
LabelIstioDataplaneMode: "ambient",
}, nil)

labeled, err := reconciler.ensureIstioMeshLabels(ctx, ns.Name)
Expect(err).NotTo(HaveOccurred())
Expect(labeled).To(BeTrue())

updated := &corev1.Namespace{}
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(ns), updated)).To(Succeed())
Expect(updated.Labels[LabelIstioDiscovery]).To(Equal("enabled"))
Expect(updated.Labels[LabelIstioDataplaneMode]).To(Equal("ambient"))
})

It("should skip namespace with opt-out annotation", func() {
ns := newTestNamespace(nil, map[string]string{
AnnotationIstioMeshOptOut: "disabled",
})

labeled, err := reconciler.ensureIstioMeshLabels(ctx, ns.Name)
Expect(err).NotTo(HaveOccurred())
Expect(labeled).To(BeFalse())

updated := &corev1.Namespace{}
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(ns), updated)).To(Succeed())
Expect(updated.Labels).NotTo(HaveKey(LabelIstioDiscovery))
Expect(updated.Labels).NotTo(HaveKey(LabelIstioDataplaneMode))
})

It("should preserve existing labels", func() {
ns := newTestNamespace(map[string]string{
"team": "platform",
"env": "test",
}, nil)

labeled, err := reconciler.ensureIstioMeshLabels(ctx, ns.Name)
Expect(err).NotTo(HaveOccurred())
Expect(labeled).To(BeTrue())

updated := &corev1.Namespace{}
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(ns), updated)).To(Succeed())
Expect(updated.Labels["team"]).To(Equal("platform"))
Expect(updated.Labels["env"]).To(Equal("test"))
Expect(updated.Labels[LabelIstioDiscovery]).To(Equal("enabled"))
Expect(updated.Labels[LabelIstioDataplaneMode]).To(Equal("ambient"))
})

It("should label namespace when annotation value is not 'disabled'", func() {
ns := newTestNamespace(nil, map[string]string{
AnnotationIstioMeshOptOut: "enabled",
})

labeled, err := reconciler.ensureIstioMeshLabels(ctx, ns.Name)
Expect(err).NotTo(HaveOccurred())
Expect(labeled).To(BeTrue())

updated := &corev1.Namespace{}
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(ns), updated)).To(Succeed())
Expect(updated.Labels[LabelIstioDiscovery]).To(Equal("enabled"))
Expect(updated.Labels[LabelIstioDataplaneMode]).To(Equal("ambient"))
})
})
Loading
Loading