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
5 changes: 5 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,11 @@ Only named PodSpec declarations are included: implicit workload defaults and
template-generated PVC names are not inferred. An absent omission does not by
itself establish cache freshness or complete namespace coverage.

Resource context's `ingressSummary.tlsSecrets` lists named TLS Secrets in the
Ingress's namespace. A Secret's `referencedBy` includes readable Ingresses with
the path `spec.tls[].secretName`. Resource-view topology links only observed,
visible TLS Secrets. Empty Secret names do not resolve controller defaults.

### Write Tools

| Tool | Description | Parameters |
Expand Down
21 changes: 21 additions & 0 deletions pkg/configrefs/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,24 @@ func IngressBackendServices(ing *networkingv1.Ingress) []string {
}
return names
}

// IngressTLSSecretReferences returns named TLS Secret declarations in the
// Ingress's namespace. It does not infer controller defaults or TLS readiness.
func IngressTLSSecretReferences(ing *networkingv1.Ingress) []Ref {
if ing == nil {
return nil
}
var refs []Ref
seen := map[Ref]bool{}
for _, tls := range ing.Spec.TLS {
if tls.SecretName == "" {
continue
}
ref := Ref{Kind: "Secret", Namespace: ing.Namespace, Name: tls.SecretName}
if !seen[ref] {
refs = append(refs, ref)
seen[ref] = true
}
}
return refs
}
23 changes: 23 additions & 0 deletions pkg/configrefs/ingress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package configrefs

import (
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"reflect"
"testing"
)

func TestIngressTLSSecretReferences(t *testing.T) {
ing := &networkingv1.Ingress{ObjectMeta: metav1.ObjectMeta{Namespace: "a"}, Spec: networkingv1.IngressSpec{TLS: []networkingv1.IngressTLS{{SecretName: "tls"}, {SecretName: "tls"}, {}}}}
before := ing.DeepCopy()
refs := IngressTLSSecretReferences(ing)
if len(refs) != 1 || refs[0] != (Ref{Kind: "Secret", Namespace: "a", Name: "tls"}) {
t.Fatalf("refs: %+v", refs)
}
if !reflect.DeepEqual(before, ing) {
t.Fatal("input mutated")
}
if refs := IngressTLSSecretReferences(nil); refs != nil {
t.Fatalf("nil: %+v", refs)
}
}
21 changes: 19 additions & 2 deletions pkg/resourcecontext/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,23 @@ func buildReferencedBy(ctx context.Context, obj runtime.Object, provider topolog
refs = append(refs, ref)
}

if target.kind == "Secret" {
if ingresses, err := provider.Ingresses(); err != nil {
omitted.add("referencedBy", OmittedUnavailable)
} else {
for _, ing := range ingresses {
if ing == nil || ing.Namespace != target.namespace {
continue
}
for _, ref := range configrefs.IngressTLSSecretReferences(ing) {
if ref.Name == target.name {
appendRef(ReferenceUse{Kind: "Ingress", Group: "networking.k8s.io", Namespace: ing.Namespace, Name: ing.Name, Paths: []string{"spec.tls[].secretName"}})
}
}
}
}
}

if deployments, err := provider.Deployments(); err != nil {
omitted.add("referencedBy", OmittedUnavailable)
} else {
Expand Down Expand Up @@ -1025,8 +1042,8 @@ func buildIngressSummary(ctx context.Context, obj runtime.Object, ac RefAccessCh
out.BackendServices = filterRefs(ctx, ac, svcSet.refs("Service", ""), "ingressSummary.backendServices", omitted)

secretSet := newRefSet()
for _, tls := range ing.Spec.TLS {
secretSet.add(tls.SecretName, ing.Namespace)
for _, ref := range configrefs.IngressTLSSecretReferences(ing) {
secretSet.add(ref.Name, ref.Namespace)
}
out.TLSSecrets = filterRefs(ctx, ac, secretSet.refs("Secret", ""), "ingressSummary.tlsSecrets", omitted)

Expand Down
3 changes: 2 additions & 1 deletion pkg/resourcecontext/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func (m mockServiceBackends) PodsForServiceSelector(namespace string, selector l
}

type mockResourceProvider struct {
ingresses []*networkingv1.Ingress
pods []*corev1.Pod
deploys []*appsv1.Deployment
replicaSets []*appsv1.ReplicaSet
Expand All @@ -88,7 +89,7 @@ func (m mockResourceProvider) StatefulSets() ([]*appsv1.StatefulSet, error) {
func (m mockResourceProvider) ReplicaSets() ([]*appsv1.ReplicaSet, error) { return m.replicaSets, nil }
func (m mockResourceProvider) Jobs() ([]*batchv1.Job, error) { return m.jobs, nil }
func (m mockResourceProvider) CronJobs() ([]*batchv1.CronJob, error) { return m.cronJobs, nil }
func (m mockResourceProvider) Ingresses() ([]*networkingv1.Ingress, error) { return nil, nil }
func (m mockResourceProvider) Ingresses() ([]*networkingv1.Ingress, error) { return m.ingresses, nil }
func (m mockResourceProvider) ConfigMaps() ([]*corev1.ConfigMap, error) { return nil, nil }
func (m mockResourceProvider) Secrets() ([]*corev1.Secret, error) { return nil, nil }
func (m mockResourceProvider) PersistentVolumeClaims() ([]*corev1.PersistentVolumeClaim, error) {
Expand Down
50 changes: 50 additions & 0 deletions pkg/resourcecontext/ingress_tls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package resourcecontext

import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"slices"
"testing"
)

type unavailableIngressProvider struct{ mockResourceProvider }

func (p unavailableIngressProvider) Ingresses() ([]*networkingv1.Ingress, error) {
return p.ingresses, fmt.Errorf("unavailable fixture source")
}

func TestBuild_IngressTLSUnavailableSource(t *testing.T) {
ing := &networkingv1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "entry", Namespace: "a"}, Spec: networkingv1.IngressSpec{TLS: []networkingv1.IngressTLS{{SecretName: "tls"}}}}
secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "tls", Namespace: "a"}}
rc := Build(context.Background(), secret, Options{Tier: TierBasic, Provider: unavailableIngressProvider{mockResourceProvider{ingresses: []*networkingv1.Ingress{ing}}}})
if rc.ReferencedBy != nil || !slices.Contains(rc.Omitted, OmittedField{Field: "referencedBy", Reason: OmittedUnavailable}) {
t.Fatalf("failed source masqueraded as empty/success: %+v", rc)
}
}

func TestBuild_IngressTLSReverseAndForward(t *testing.T) {
ing := &networkingv1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "entry", Namespace: "a"}, Spec: networkingv1.IngressSpec{TLS: []networkingv1.IngressTLS{{SecretName: "tls"}, {SecretName: "tls"}, {}}}}
other := ing.DeepCopy()
other.Namespace = "b"
secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "tls", Namespace: "a"}}
provider := mockResourceProvider{ingresses: []*networkingv1.Ingress{nil, other, ing}}
rc := Build(context.Background(), secret, Options{Tier: TierBasic, Provider: provider})
if rc.ReferencedBy == nil || rc.ReferencedBy.Total != 1 {
t.Fatalf("reverse: %+v", rc)
}
ref := rc.ReferencedBy.Items[0]
if ref.Kind != "Ingress" || ref.Group != "networking.k8s.io" || ref.Namespace != "a" || len(ref.Paths) != 1 || ref.Paths[0] != "spec.tls[].secretName" {
t.Fatalf("TLS provenance: %+v", ref)
}
rc = Build(context.Background(), secret, Options{Tier: TierBasic, Provider: provider, AccessChecker: denyChecker{kind: "Ingress", group: "networking.k8s.io", namespace: "a"}})
if rc.ReferencedBy != nil || !hasOmitted(rc.Omitted, "referencedBy") {
t.Fatalf("denied ingress: %+v", rc)
}
rc = Build(context.Background(), ing, Options{Tier: TierBasic})
if rc.IngressSummary == nil || len(rc.IngressSummary.TLSSecrets) != 1 || rc.IngressSummary.TLSSecrets[0].Namespace != "a" {
t.Fatalf("forward: %+v", rc)
}
}
6 changes: 6 additions & 0 deletions pkg/topology/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3674,6 +3674,12 @@ func (b *Builder) buildResourcesTopology(opts BuildOptions) (*Topology, error) {
},
})

refs := workloadRefs{secrets: make(map[string]bool)}
for _, ref := range configrefs.IngressTLSSecretReferences(ing) {
refs.secrets[ref.Name] = true
}
trackWorkloadRefs(ingID, ing.Namespace, refs)

// Connect to backend Services
for _, svcName := range configrefs.IngressBackendServices(ing) {
if svcID, ok := serviceIDs[ing.Namespace+"/"+svcName]; ok {
Expand Down
63 changes: 63 additions & 0 deletions pkg/topology/ingress_tls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package topology

import (
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"testing"
)

func TestIngressTLSSecretEdgesRespectNamespaceAndVisibility(t *testing.T) {
ing := &networkingv1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "entry", Namespace: "a"}, Spec: networkingv1.IngressSpec{TLS: []networkingv1.IngressTLS{{SecretName: "tls"}, {SecretName: "tls"}, {}}}}
other := ing.DeepCopy()
other.Namespace = "b"
provider := &mockProvider{ingresses: []*networkingv1.Ingress{ing, other}, secrets: []*corev1.Secret{{ObjectMeta: metav1.ObjectMeta{Name: "tls", Namespace: "a"}}, {ObjectMeta: metav1.ObjectMeta{Name: "tls", Namespace: "b"}}}}
opts := DefaultBuildOptions()
opts.Namespaces = []string{"a"}
opts.IncludeSecrets = true
graph, err := NewBuilder(provider).Build(opts)
if err != nil {
t.Fatal(err)
}
count := 0
for _, edge := range graph.Edges {
if edge.Source == "secret/a/tls" && edge.Target == "ingress/a/entry" && edge.Type == EdgeConfigures {
count++
}
if edge.Source == "secret/b/tls" {
t.Fatalf("cross-namespace Secret edge: %+v", edge)
}
}
if count != 1 {
t.Fatalf("TLS edges: %+v", graph.Edges)
}
rel := GetRelationships("Ingress", "a", "entry", graph, provider, nil)
if rel == nil || len(rel.ConfigRefs) != 1 || rel.ConfigRefs[0].Name != "tls" || rel.ConfigRefs[0].Namespace != "a" {
t.Fatalf("Ingress refs: %+v", rel)
}
rel = GetRelationships("Secret", "a", "tls", graph, provider, nil)
if rel == nil || len(rel.Consumers) != 1 || rel.Consumers[0].Kind != "Ingress" || rel.Consumers[0].Group != "networking.k8s.io" {
t.Fatalf("reverse refs: %+v", rel)
}
opts.IncludeSecrets = false
graph, err = NewBuilder(provider).Build(opts)
if err != nil {
t.Fatal(err)
}
for _, node := range graph.Nodes {
if node.Kind == KindSecret {
t.Fatalf("Secret visibility ignored: %+v", node)
}
}
provider.secrets = nil
opts.IncludeSecrets = true
graph, err = NewBuilder(provider).Build(opts)
if err != nil {
t.Fatal(err)
}
for _, edge := range graph.Edges {
if edge.Source == "secret/a/tls" {
t.Fatalf("unobserved Secret fabricated: %+v", edge)
}
}
}
Loading