Skip to content

Commit d66825d

Browse files
authored
Resolve Contour's delegated TLS secrets in their own namespace (#2073)
Contour lets an HTTPProxy terminate TLS with a certificate kept in another namespace. That namespace grants access with a `TLSCertificateDelegation`, and the proxy names the Secret as `namespace/name` in `spec.virtualhost.tls.secretName`. Radar treated that whole string as a Secret name in the proxy's namespace, which caused three problems: - **Topology** drew an edge to `secret/<proxy-ns>/<cert-ns>/<name>`, a Secret that doesn't exist, and created a stub node for it in the proxy's namespace. - **The HTTPProxy drawer's** TLS link and Related Resources pointed to a Secret named `<cert-ns>` in the proxy's namespace. The link opened "Resource not found". - **The audit** reported the delegated certificate as `orphanConfigMapSecret`, even though Contour serves it. The audit also missed Secrets that HTTPProxies reference in other fields: client-validation CA and CRL Secrets, upstream-validation CA Secrets, and the JWKS CA. It reported those Secrets as unused too. ## What changed - **One parser, matching Contour.** `configrefs.ContourSecretRef` (Go) and `getHTTPProxyTLSSecret` (k8s-ui) resolve a reference the way Contour's `k8s.NamespacedNameFrom` does, with the proxy's namespace as the default. A bare name, or an empty namespace part, stays in the proxy's namespace; otherwise the part before the first `/` is the namespace. - **Topology:** the TLS edge, the stub node and the cert-manager Certificate lookup now use the Secret's real namespace. In a namespace-filtered view, a delegated Secret and its edge appear only when its namespace is selected, like any other node. - **Drawer:** the TLS link opens the right Secret, and a delegated one shows as `namespace/name`. - **Audit — references:** it now reads every Secret reference Contour resolves this way: - `virtualhost.tls.secretName`; - `tls.clientValidation.caSecret` and `crlSecret`; - `jwtProviders[].remoteJWKS.validation.caSecret`; - `routes[].services[].validation.caSecret` and `tcpproxy.services[].validation.caSecret`. - **Audit — cross-namespace source:** HTTPProxies are now marked as a cross-namespace reference source, like Gateways, so auditing only the certificate's namespace still reads the proxies that use it. - Side effect, the same one Gateways already have: under a namespace-restricted read scope, Radar can't see every proxy that might reference a Secret. On clusters with the HTTPProxy API, it therefore stops claiming Secrets are unused rather than guess. ## Result on a live cluster Contour 1.33.7 on an EKS test cluster. The proxy `shop` in `radar-contour` uses `radar-contour-certs/wildcard`, and Envoy serves it over TLS. | | Before | After | |---|---|---| | Topology edge from `shop` | `secret/radar-contour/radar-contour-certs/wildcard` (stub in `radar-contour`) | `secret/radar-contour-certs/wildcard` | | Drawer: Related Resources | `secret/radar-contour-certs` | `secret/wildcard` | | Drawer: TLS link | "Resource not found" | Secret `wildcard` with its certificate details | | Audit of `radar-contour-certs` | `wildcard`: `orphanConfigMapSecret` | no finding | To confirm the audit wasn't just silenced, I added a genuinely unused Secret to the same namespace. It was still flagged. | TLS link, before | TLS link, after | |---|---| | ![Before: the TLS link opens "Resource not found" for radar-contour-certs/wildcard in radar-contour](https://github.com/user-attachments/assets/6f95ba97-3216-481c-b13d-aa1fb74fe64c) | ![After: the TLS link opens Secret wildcard in radar-contour-certs with its certificate details](https://github.com/user-attachments/assets/f46f823b-0767-436b-acc1-f66eef45c177) | ## Testing - **Parsers:** `TestContourSecretRef` and `resource-utils-contour.test.ts` cover a bare name, `ns/name`, `/name` and `a/b/c`. - **Topology:** `TestHTTPProxyTLSSecretResolvesDelegatedNamespace` checks the edges, the stub's namespace and the Certificate edge. `TestHTTPProxyDelegatedSecretRespectsNamespaceFilter` checks the node and edge in and out of the filter. - **Audit:** - `TestDynamicConfigObjectRefs` covers every reference path. - `TestListDynamicConfigObjectRefsFindsDelegatedContourSecret` covers the cross-namespace read. - `TestDelegatedContourCertificateIsNotOrphan` covers the full audit: unrestricted, the unused Secret is flagged and the delegated one isn't; under restricted scope, neither is claimed. - Each test fails with its fix reverted. - `make tsc` and `make test` pass. ## Notes The Related Resources panel files this Secret under "Used By" and Services under "Pods". That's how relationships currently map HTTPProxy edges, both before and after this PR; it's tracked with the edge-direction work in RAD-695.
1 parent 8f9d0b6 commit d66825d

10 files changed

Lines changed: 338 additions & 10 deletions

File tree

‎internal/audit/config_evidence_test.go‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,60 @@ func TestReflectionCoverageHonorsCompleteNamespaceSecretGrants(t *testing.T) {
295295
}
296296
}
297297
}
298+
299+
func TestDelegatedContourCertificateIsNotOrphan(t *testing.T) {
300+
wildcard := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wildcard", Namespace: "certs"}}
301+
unused := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "unused", Namespace: "certs"}}
302+
client := fake.NewClientset(wildcard, unused)
303+
scopes := map[string]k8score.ResourceScope{}
304+
for _, kind := range append(append([]string{}, typedConfigConsumers...), "secrets", "configmaps") {
305+
scopes[kind] = k8score.ResourceScope{Enabled: true}
306+
}
307+
if err := k8s.InitScopedTestResourceCache(client, scopes); err != nil {
308+
t.Fatal(err)
309+
}
310+
t.Cleanup(k8s.ResetTestState)
311+
312+
proxyGVR := schema.GroupVersionResource{Group: "projectcontour.io", Version: "v1", Resource: "httpproxies"}
313+
proxy := testUnstructured("projectcontour.io/v1", "HTTPProxy", "shop", "storefront", map[string]any{
314+
"spec": map[string]any{"virtualhost": map[string]any{
315+
"fqdn": "shop.example.com",
316+
"tls": map[string]any{"secretName": "certs/wildcard"},
317+
}},
318+
})
319+
dyn := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), map[schema.GroupVersionResource]string{proxyGVR: "HTTPProxyList"})
320+
if _, err := dyn.Resource(proxyGVR).Namespace("shop").Create(context.Background(), proxy, metav1.CreateOptions{}); err != nil {
321+
t.Fatal(err)
322+
}
323+
if err := k8s.InitTestDynamicResourceCache(dyn, []k8s.APIResource{{Group: proxyGVR.Group, Version: proxyGVR.Version, Name: proxyGVR.Resource, Kind: "HTTPProxy", Namespaced: true, Verbs: []string{"list", "watch"}}}); err != nil {
324+
t.Fatal(err)
325+
}
326+
t.Cleanup(k8s.ResetTestDynamicState)
327+
dynamic := k8s.GetDynamicResourceCache()
328+
if err := dynamic.EnsureWatching(proxyGVR); err != nil {
329+
t.Fatal(err)
330+
}
331+
if !dynamic.WaitForSync(proxyGVR, 2*time.Second) {
332+
t.Fatal("sync timeout")
333+
}
334+
cache := k8s.GetResourceCache()
335+
orphans := func(results *bp.ScanResults) map[string]bool {
336+
seen := map[string]bool{}
337+
for _, f := range results.Findings {
338+
if f.CheckID == "orphanConfigMapSecret" && f.Kind == "Secret" {
339+
seen[f.Name] = true
340+
}
341+
}
342+
return seen
343+
}
344+
345+
// Auditing only the certificate's namespace still sees the proxy in "shop".
346+
if seen := orphans(RunFromCache(cache, []string{"certs"}, nil)); !seen["unused"] || seen["wildcard"] {
347+
t.Fatalf("unrestricted audit of certs: orphans %v, want only unused", seen)
348+
}
349+
// Without read access to other namespaces' proxies, absence can't be proven.
350+
limited := &ReadScope{Namespaces: []string{"certs"}, SecretNamespaces: []string{"certs"}}
351+
if seen := orphans(RunFromCache(cache, []string{"certs"}, &RunOptions{Scope: limited})); len(seen) != 0 {
352+
t.Fatalf("restricted audit claimed orphans it cannot prove: %v", seen)
353+
}
354+
}

‎internal/audit/crd_config_refs.go‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"slices"
66
"strings"
77

8+
"github.com/skyhook-io/radar/pkg/configrefs"
89
"github.com/skyhook-io/radar/pkg/resourceid"
910

1011
appsv1 "k8s.io/api/apps/v1"
@@ -141,7 +142,7 @@ var configRefDependencies = map[schema.GroupResource]configRefDependency{
141142
{Group: "traefik.containo.us", Resource: "tlsoptions"}: {traefikTLSOptionConfigRefs, false, false},
142143
{Group: "traefik.io", Resource: "tlsstores"}: {traefikTLSStoreConfigRefs, false, false},
143144
{Group: "traefik.containo.us", Resource: "tlsstores"}: {traefikTLSStoreConfigRefs, false, false},
144-
{Group: "projectcontour.io", Resource: "httpproxies"}: {contourHTTPProxyConfigRefs, false, false},
145+
{Group: "projectcontour.io", Resource: "httpproxies"}: {contourHTTPProxyConfigRefs, false, true},
145146
{Group: "networking.istio.io", Resource: "gateways"}: {istioGatewayConfigRefs, false, false},
146147
{Group: "cert-manager.io", Resource: "certificates"}: {certManagerCertificateConfigRefs, false, false},
147148
{Group: "cert-manager.io", Resource: "issuers"}: {certManagerIssuerConfigRefs, false, false},
@@ -243,7 +244,26 @@ func traefikTLSStoreConfigRefs(u *unstructured.Unstructured) []bp.ConfigObjectRe
243244

244245
func contourHTTPProxyConfigRefs(u *unstructured.Unstructured) []bp.ConfigObjectRef {
245246
var refs []bp.ConfigObjectRef
246-
addSecret(&refs, u.GetNamespace(), stringAt(u.Object, "spec", "virtualhost", "tls", "secretName"))
247+
add := func(ref string) {
248+
if ref != "" {
249+
ns, name := configrefs.ContourSecretRef(u.GetNamespace(), ref)
250+
addSecret(&refs, ns, name)
251+
}
252+
}
253+
add(stringAt(u.Object, "spec", "virtualhost", "tls", "secretName"))
254+
add(stringAt(u.Object, "spec", "virtualhost", "tls", "clientValidation", "caSecret"))
255+
add(stringAt(u.Object, "spec", "virtualhost", "tls", "clientValidation", "crlSecret"))
256+
for _, provider := range mapsAt(u.Object, "spec", "virtualhost", "jwtProviders") {
257+
add(stringAt(provider, "remoteJWKS", "validation", "caSecret"))
258+
}
259+
for _, route := range mapsAt(u.Object, "spec", "routes") {
260+
for _, service := range mapsAt(route, "services") {
261+
add(stringAt(service, "validation", "caSecret"))
262+
}
263+
}
264+
for _, service := range mapsAt(u.Object, "spec", "tcpproxy", "services") {
265+
add(stringAt(service, "validation", "caSecret"))
266+
}
247267
return refs
248268
}
249269

‎internal/audit/crd_config_refs_test.go‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,34 @@ func TestDynamicConfigObjectRefs(t *testing.T) {
6868
obj: map[string]any{"spec": map[string]any{"virtualhost": map[string]any{"tls": map[string]any{"secretName": "contour-cert"}}}},
6969
want: refs(secret("edge", "contour-cert")),
7070
},
71+
{
72+
name: "contour httpproxy delegated tls",
73+
gvr: gvr("projectcontour.io", "v1", "httpproxies"),
74+
ns: "edge",
75+
obj: map[string]any{"spec": map[string]any{"virtualhost": map[string]any{"tls": map[string]any{"secretName": "certs/wildcard"}}}},
76+
want: refs(secret("certs", "wildcard")),
77+
},
78+
{
79+
name: "contour httpproxy validation secrets",
80+
gvr: gvr("projectcontour.io", "v1", "httpproxies"),
81+
ns: "edge",
82+
obj: map[string]any{"spec": map[string]any{
83+
"virtualhost": map[string]any{
84+
"tls": map[string]any{
85+
"secretName": "serving",
86+
"clientValidation": map[string]any{"caSecret": "client-ca", "crlSecret": "pki/client-crl"},
87+
},
88+
"jwtProviders": []any{map[string]any{"remoteJWKS": map[string]any{"validation": map[string]any{"caSecret": "jwks-ca"}}}},
89+
},
90+
"routes": []any{map[string]any{"services": []any{
91+
map[string]any{"name": "api", "validation": map[string]any{"caSecret": "pki/upstream-ca"}},
92+
}}},
93+
"tcpproxy": map[string]any{"services": []any{
94+
map[string]any{"name": "db", "validation": map[string]any{"caSecret": "db-ca"}},
95+
}},
96+
}},
97+
want: refs(secret("edge", "serving"), secret("edge", "client-ca"), secret("pki", "client-crl"), secret("edge", "jwks-ca"), secret("pki", "upstream-ca"), secret("edge", "db-ca")),
98+
},
7199
{
72100
name: "istio gateway tls credentials",
73101
gvr: gvr("networking.istio.io", "v1", "gateways"),
@@ -371,6 +399,41 @@ func TestListDynamicConfigObjectRefsFiltersByTargetNamespace(t *testing.T) {
371399
assertRefSet(t, got, refs(secret("infra", "shared-cert")))
372400
}
373401

402+
func TestListDynamicConfigObjectRefsFindsDelegatedContourSecret(t *testing.T) {
403+
proxyGVR := gvr("projectcontour.io", "v1", "httpproxies")
404+
proxy := testUnstructured("projectcontour.io/v1", "HTTPProxy", "shop", "storefront", map[string]any{
405+
"spec": map[string]any{"virtualhost": map[string]any{
406+
"fqdn": "shop.example.com",
407+
"tls": map[string]any{"secretName": "certs/wildcard"},
408+
}},
409+
})
410+
dynClient := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(
411+
runtime.NewScheme(),
412+
map[schema.GroupVersionResource]string{proxyGVR: "HTTPProxyList"},
413+
)
414+
if _, err := dynClient.Resource(proxyGVR).Namespace("shop").Create(context.Background(), proxy, metav1.CreateOptions{}); err != nil {
415+
t.Fatalf("create HTTPProxy fixture: %v", err)
416+
}
417+
if err := k8s.InitTestDynamicResourceCache(dynClient, []k8s.APIResource{
418+
{Group: "projectcontour.io", Version: "v1", Kind: "HTTPProxy", Name: "httpproxies", Namespaced: true, Verbs: []string{"list", "watch"}},
419+
}); err != nil {
420+
t.Fatalf("InitTestDynamicResourceCache: %v", err)
421+
}
422+
defer k8s.ResetTestDynamicState()
423+
424+
dynCache := k8s.GetDynamicResourceCache()
425+
if err := dynCache.EnsureWatching(proxyGVR); err != nil {
426+
t.Fatalf("EnsureWatching: %v", err)
427+
}
428+
if !dynCache.WaitForSync(proxyGVR, 2*time.Second) {
429+
t.Fatal("dynamic HTTPProxy cache did not sync")
430+
}
431+
432+
// Auditing only the delegating namespace must still see the proxy that uses its Secret.
433+
got := listDynamicConfigObjectRefs([]string{"certs"}, dynamicConfigRefOptions{})
434+
assertRefSet(t, got, refs(secret("certs", "wildcard")))
435+
}
436+
374437
func TestListDynamicConfigObjectRefsAddsServiceAccountImagePullSecretsForPodTemplates(t *testing.T) {
375438
rolloutGVR := gvr("argoproj.io", "v1alpha1", "rollouts")
376439
rollout := testUnstructured("argoproj.io/v1alpha1", "Rollout", "app", "web", map[string]any{

‎packages/k8s-ui/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getHTTPProxyRouteCount,
66
getHTTPProxyServiceCount,
77
getHTTPProxyStatus,
8+
getHTTPProxyTLSSecret,
89
hasHTTPProxyTLS,
910
} from '../resource-utils-contour'
1011

@@ -19,6 +20,7 @@ export function ContourHTTPProxyRenderer({ data, onNavigate }: ContourHTTPProxyR
1920
const routes = spec.routes || []
2021
const includes = spec.includes || []
2122
const tls = spec.virtualhost?.tls
23+
const tlsSecret = getHTTPProxyTLSSecret(data)
2224
const tcpproxy = spec.tcpproxy
2325
const ns = data.metadata?.namespace || ''
2426
const conditions = status.conditions
@@ -174,12 +176,13 @@ export function ContourHTTPProxyRenderer({ data, onNavigate }: ContourHTTPProxyR
174176
{tls && (
175177
<Section title="TLS" icon={Lock} defaultExpanded>
176178
<PropertyList>
177-
{tls.secretName && (
179+
{tlsSecret && (
178180
<Property label="Secret" value={
179181
<ResourceLink
180-
name={tls.secretName}
182+
name={tlsSecret.name}
181183
kind="secrets"
182-
namespace={ns}
184+
namespace={tlsSecret.namespace}
185+
label={tlsSecret.namespace !== ns ? `${tlsSecret.namespace}/${tlsSecret.name}` : undefined}
183186
onNavigate={onNavigate}
184187
/>
185188
} />
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { getHTTPProxyTLSSecret } from './resource-utils-contour'
3+
4+
const proxy = (secretName?: string) => ({
5+
metadata: { namespace: 'shop' },
6+
spec: { virtualhost: { fqdn: 'shop.example.com', tls: secretName === undefined ? {} : { secretName } } },
7+
})
8+
9+
describe('getHTTPProxyTLSSecret', () => {
10+
it('resolves a bare name in the proxy namespace', () => {
11+
expect(getHTTPProxyTLSSecret(proxy('cert'))).toEqual({ namespace: 'shop', name: 'cert' })
12+
})
13+
it('resolves a delegated namespace/name', () => {
14+
expect(getHTTPProxyTLSSecret(proxy('certs/wildcard'))).toEqual({ namespace: 'certs', name: 'wildcard' })
15+
})
16+
it('defaults an empty namespace to the proxy namespace', () => {
17+
expect(getHTTPProxyTLSSecret(proxy('/cert'))).toEqual({ namespace: 'shop', name: 'cert' })
18+
})
19+
it('splits only at the first slash', () => {
20+
expect(getHTTPProxyTLSSecret(proxy('certs/a/b'))).toEqual({ namespace: 'certs', name: 'a/b' })
21+
})
22+
it('returns null without a secret', () => {
23+
expect(getHTTPProxyTLSSecret(proxy())).toBeNull()
24+
})
25+
})

‎packages/k8s-ui/src/components/resources/resource-utils-contour.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,15 @@ export function getHTTPProxyIncludeCount(resource: any): number {
3232
export function hasHTTPProxyTLS(resource: any): boolean {
3333
return !!resource.spec?.virtualhost?.tls
3434
}
35+
36+
// Contour reads tls.secretName as "namespace/name" when another namespace
37+
// delegates the Secret with a TLSCertificateDelegation; a bare name, or an
38+
// empty namespace, is in the proxy's own namespace.
39+
export function getHTTPProxyTLSSecret(resource: any): { namespace: string; name: string } | null {
40+
const ref: string | undefined = resource.spec?.virtualhost?.tls?.secretName
41+
if (!ref) return null
42+
const ownNamespace = resource.metadata?.namespace ?? ''
43+
const slash = ref.indexOf('/')
44+
if (slash < 0) return { namespace: ownNamespace, name: ref }
45+
return { namespace: ref.slice(0, slash) || ownNamespace, name: ref.slice(slash + 1) }
46+
}

‎pkg/configrefs/contour.go‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package configrefs
2+
3+
import "strings"
4+
5+
// ContourSecretRef resolves a Secret reference in an HTTPProxy, such as
6+
// spec.virtualhost.tls.secretName, the way Contour does. "namespace/name"
7+
// names a Secret another namespace delegates with a TLSCertificateDelegation;
8+
// a bare name, or an empty namespace, is in the proxy's own namespace.
9+
func ContourSecretRef(proxyNamespace, ref string) (namespace, name string) {
10+
namespace, name, found := strings.Cut(ref, "/")
11+
if !found {
12+
return proxyNamespace, ref
13+
}
14+
if namespace == "" {
15+
namespace = proxyNamespace
16+
}
17+
return namespace, name
18+
}

‎pkg/configrefs/contour_test.go‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package configrefs
2+
3+
import "testing"
4+
5+
func TestContourSecretRef(t *testing.T) {
6+
for _, tt := range []struct{ ref, wantNs, wantName string }{
7+
{ref: "cert", wantNs: "edge", wantName: "cert"},
8+
{ref: "certs/wildcard", wantNs: "certs", wantName: "wildcard"},
9+
{ref: "/cert", wantNs: "edge", wantName: "cert"},
10+
{ref: "certs/a/b", wantNs: "certs", wantName: "a/b"},
11+
} {
12+
ns, name := ContourSecretRef("edge", tt.ref)
13+
if ns != tt.wantNs || name != tt.wantName {
14+
t.Errorf("ContourSecretRef(edge, %q) = %s/%s, want %s/%s", tt.ref, ns, name, tt.wantNs, tt.wantName)
15+
}
16+
}
17+
}

‎pkg/topology/builder.go‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5719,9 +5719,11 @@ func (b *Builder) buildResourcesTopology(opts BuildOptions) (*Topology, error) {
57195719
}
57205720

57215721
// HTTPProxy → Secret edges (via spec.virtualhost.tls.secretName)
5722-
tlsSecretName, _, _ := unstructured.NestedString(res.Object, "spec", "virtualhost", "tls", "secretName")
5723-
if tlsSecretName != "" {
5724-
secretNodeID := fmt.Sprintf("secret/%s/%s", resNs, tlsSecretName)
5722+
tlsSecretRef, _, _ := unstructured.NestedString(res.Object, "spec", "virtualhost", "tls", "secretName")
5723+
secretNs, tlsSecretName := configrefs.ContourSecretRef(resNs, tlsSecretRef)
5724+
// A delegated certificate lives in another namespace, which a filtered view may exclude.
5725+
if tlsSecretName != "" && opts.MatchesNamespaceFilter(secretNs) {
5726+
secretNodeID := fmt.Sprintf("secret/%s/%s", secretNs, tlsSecretName)
57255727
// Create stub Secret node if it doesn't already exist (same pattern as Traefik)
57265728
if !existingSecretNodes[secretNodeID] {
57275729
existingSecretNodes[secretNodeID] = true
@@ -5731,7 +5733,7 @@ func (b *Builder) buildResourcesTopology(opts BuildOptions) (*Topology, error) {
57315733
Name: tlsSecretName,
57325734
Status: StatusUnknown,
57335735
Data: map[string]any{
5734-
"namespace": resNs,
5736+
"namespace": secretNs,
57355737
"labels": map[string]string{},
57365738
},
57375739
})
@@ -5747,7 +5749,7 @@ func (b *Builder) buildResourcesTopology(opts BuildOptions) (*Topology, error) {
57475749
})
57485750
}
57495751
// Also check for cert-manager Certificate edge
5750-
certID := certBySecret[resNs+"/"+tlsSecretName]
5752+
certID := certBySecret[secretNs+"/"+tlsSecretName]
57515753
if certID != "" {
57525754
dedupeKey2 := resID + "|" + certID
57535755
if !contourEdgeSeen[dedupeKey2] {

0 commit comments

Comments
 (0)