diff --git a/internal/kube/site/attached_connector.go b/internal/kube/site/attached_connector.go index bbe7b7bbc..757eee62d 100644 --- a/internal/kube/site/attached_connector.go +++ b/internal/kube/site/attached_connector.go @@ -304,6 +304,9 @@ func (a *AttachedConnector) updateBridgeConfig(siteId string, config *qdr.Bridge if definition == nil || a.watcher == nil { return updated } + if definition.Spec.TlsCredentials != "" && !a.parent.bindings.TlsCredentialIncluded(definition.Spec.TlsCredentials) { + return updated + } connector := &skupperv2alpha1.Connector{ ObjectMeta: metav1.ObjectMeta{ Name: definition.Name, diff --git a/internal/kube/site/extended_bindings.go b/internal/kube/site/extended_bindings.go index d283fec79..2af79bd65 100644 --- a/internal/kube/site/extended_bindings.go +++ b/internal/kube/site/extended_bindings.go @@ -406,6 +406,9 @@ func (b *ExtendedBindings) Apply(config *qdr.RouterConfig) bool { } } for _, ptl := range b.perTargetListeners { + if ptl.definition.Spec.TlsCredentials != "" && !b.bindings.TlsCredentialIncluded(ptl.definition.Spec.TlsCredentials) { + continue + } if ptl.updateBridgeConfig(b.bindings.SiteId, &desired) { updated = true } @@ -425,6 +428,9 @@ func (b *ExtendedBindings) Apply(config *qdr.RouterConfig) bool { func (b *ExtendedBindings) AddSslProfiles(config *qdr.RouterConfig, definitions map[string]*skupperv2alpha1.AttachedConnector) bool { profiles := map[string]qdr.SslProfile{} for _, c := range definitions { + if c.Spec.TlsCredentials != "" && !b.bindings.TlsCredentialIncluded(c.Spec.TlsCredentials) { + continue + } if c.Spec.TlsCredentials != "" { if !c.Spec.UseClientCert { //if only ca is used, need to qualify the profile to ensure that it does not collide with @@ -451,6 +457,7 @@ func (b *ExtendedBindings) AddSslProfiles(config *qdr.RouterConfig, definitions func (b *ExtendedBindings) SetSite(site *Site) { b.bindings.SetSiteId(site.site.GetSiteId()) + b.bindings.SetTlsSecretAllowed(site.tlsCredentialSecretPresent) b.site = site } @@ -527,6 +534,9 @@ func (b *ExtendedBindings) attachedConnectorUnreferenced(namespace string, name func (b *ExtendedBindings) networkUpdated(network []skupperv2alpha1.SiteRecord) qdr.ConfigUpdate { changed := false for _, ptl := range b.perTargetListeners { + if ptl.definition.Spec.TlsCredentials != "" && !b.bindings.TlsCredentialIncluded(ptl.definition.Spec.TlsCredentials) { + continue + } update, err := ptl.extractTargets(network, b.mapping, b.exposed, b.context) if err != nil { if err := b.site.updateListenerStatus(ptl.definition, err); err != nil { diff --git a/internal/kube/site/site.go b/internal/kube/site/site.go index 8742b58eb..d3fb59763 100644 --- a/internal/kube/site/site.go +++ b/internal/kube/site/site.go @@ -90,7 +90,7 @@ func NewSite(namespace string, eventProcessor *watchers.EventProcessor, certs ce site.profiles = secrets.NewProfilesWatcher( sslSecretsWatcher(namespace, eventProcessor), eventProcessor.GetKubeClient(), - site.updateRouterConfig, + site.reconcileAfterTlsSecretChange, site, namespace, logger.With( @@ -736,6 +736,83 @@ func (s *Site) ownerReferences() []metav1.OwnerReference { } } +// tlsCredentialSecretPresent reports whether a Secret with the given name exists in the site namespace. +// On unexpected API errors it returns true so a transient failure does not strip TLS-dependent configuration +// (which would remove link connectors and break the mesh). +func (s *Site) tlsCredentialSecretPresent(secretName string) bool { + if secretName == "" { + return false + } + _, err := s.clients.GetKubeClient().CoreV1().Secrets(s.namespace).Get(context.TODO(), secretName, metav1.GetOptions{}) + if err == nil { + return true + } + if errors.IsNotFound(err) { + return false + } + s.logger.Warn("TLS secret lookup failed; assuming secret exists to avoid stripping TLS configuration", + slog.String("secret", secretName), + slog.Any("error", err)) + return true +} + +// eligibleLinksConfig applies only links whose TLS credential secrets are present. +type eligibleLinksConfig struct { + site *Site +} + +func (e *eligibleLinksConfig) Apply(config *qdr.RouterConfig) bool { + changed := false + eligible := map[string]struct{}{} + for name, link := range e.site.links { + d := link.Definition() + if d == nil { + continue + } + if d.Spec.TlsCredentials != "" && !e.site.tlsCredentialSecretPresent(d.Spec.TlsCredentials) { + continue + } + eligible[name] = struct{}{} + if link.Apply(config) { + changed = true + } + } + // Remove only connectors owned by links that are ineligible (e.g. missing TLS secret). + // Do not use site.LinkMap.Apply: its cleanup removes every non-auto-mesh connector not in the map, + // which would strip inter-router and other non-link connectors. + for name, link := range e.site.links { + if _, ok := eligible[name]; ok { + continue + } + d := link.Definition() + if d != nil && d.Spec.TlsCredentials != "" && !e.site.tlsCredentialSecretPresent(d.Spec.TlsCredentials) { + if site.NewRemoveConnector(name).Apply(config) { + changed = true + } + } + } + return changed +} + +// reconcileAfterTlsSecretChange reapplies desired router configuration when TLS-related secrets change, +// so resources that were omitted while a secret was missing are added once it exists. +func (s *Site) reconcileAfterTlsSecretChange(pw qdr.ConfigUpdate) error { + groups := s.groups() + for i, group := range groups { + op := ConfigUpdateList{ + s.bindings, + s, + s.linkAccess.DesiredConfigAllowingTlsSecrets(groups[:i], SSL_PROFILE_PATH, s.tlsCredentialSecretPresent), + &eligibleLinksConfig{site: s}, + pw, + } + if err := s.updateRouterConfigForGroup(op, group); err != nil { + return err + } + } + return nil +} + func (s *Site) recoverRouterConfig(update bool) ([]*qdr.RouterConfig, error) { list, err := s.clients.GetKubeClient().CoreV1().ConfigMaps(s.namespace).List(context.TODO(), metav1.ListOptions{ LabelSelector: "internal.skupper.io/router-config", @@ -770,7 +847,7 @@ func (s *Site) recoverRouterConfig(update bool) ([]*qdr.RouterConfig, error) { for i, group := range groups { if config, ok := byName[group]; ok { if update { - op := ConfigUpdateList{s.bindings, s, s.linkAccess.DesiredConfig(groups[:i], SSL_PROFILE_PATH)} + op := ConfigUpdateList{s.bindings, s, s.linkAccess.DesiredConfigAllowingTlsSecrets(groups[:i], SSL_PROFILE_PATH, s.tlsCredentialSecretPresent)} if err := kubeqdr.UpdateRouterConfig(s.clients.GetKubeClient(), group, s.namespace, context.TODO(), op, s.labelling); err != nil { s.logger.Error("Failed to update router config map", slog.String("namespace", s.namespace), @@ -783,7 +860,7 @@ func (s *Site) recoverRouterConfig(update bool) ([]*qdr.RouterConfig, error) { } else { routerConfig := s.initialRouterConfig() s.bindings.Apply(routerConfig) - s.linkAccess.DesiredConfig(groups[:i], SSL_PROFILE_PATH).Apply(routerConfig) + s.linkAccess.DesiredConfigAllowingTlsSecrets(groups[:i], SSL_PROFILE_PATH, s.tlsCredentialSecretPresent).Apply(routerConfig) if err := s.createRouterConfigForGroup(group, routerConfig); err != nil { s.logger.Error("Failed to create router config map", slog.String("namespace", s.namespace), @@ -1176,6 +1253,14 @@ func (s *Site) link(linkconfig *skupperv2alpha1.Link) error { config.UpdateProxyConfig(currentProxyConfig) } } + if linkconfig.Spec.TlsCredentials != "" && !s.tlsCredentialSecretPresent(linkconfig.Spec.TlsCredentials) { + s.logger.Info("Deferring link router configuration until TLS credentials secret exists", + slog.String("namespace", s.namespace), + slog.String("link", linkconfig.Name), + slog.String("secret", linkconfig.Spec.TlsCredentials), + ) + return s.updateLinkConfiguredCondition(linkconfig, fmt.Errorf("TLS credentials secret %q not found", linkconfig.Spec.TlsCredentials)) + } err := s.updateRouterConfig(config) return s.updateLinkConfiguredCondition(linkconfig, err) } else { @@ -1536,7 +1621,7 @@ func (s *Site) CheckRouterAccess(name string, la *skupperv2alpha1.RouterAccess) var errors []string for i, group := range groups { if specChanged || !la.IsConfigured() { - if err := s.updateRouterConfigForGroup(s.linkAccess.DesiredConfig(previousGroups, SSL_PROFILE_PATH), group); err != nil { + if err := s.updateRouterConfigForGroup(s.linkAccess.DesiredConfigAllowingTlsSecrets(previousGroups, SSL_PROFILE_PATH, s.tlsCredentialSecretPresent), group); err != nil { s.logger.Error("Error updating router config", slog.String("namespace", s.namespace), slog.Any("error", err)) diff --git a/internal/site/bindings.go b/internal/site/bindings.go index 72da8de09..1a2ea2cca 100644 --- a/internal/site/bindings.go +++ b/internal/site/bindings.go @@ -29,6 +29,7 @@ type Bindings struct { listeners map[string]*skupperv2alpha1.Listener multiKeyListeners map[string]*skupperv2alpha1.MultiKeyListener handler BindingEventHandler + tlsSecretAllowed func(secretName string) bool configure struct { listener ListenerConfiguration connector ConnectorConfiguration @@ -53,6 +54,22 @@ func (b *Bindings) SetSiteId(siteId string) { b.SiteId = siteId } +func (b *Bindings) SetTlsSecretAllowed(f func(string) bool) { + b.tlsSecretAllowed = f +} + +// TlsCredentialIncluded reports whether TLS material for the given secret name may be +// referenced in generated router configuration. Empty name always returns true. +func (b *Bindings) TlsCredentialIncluded(secretName string) bool { + if secretName == "" { + return true + } + if b.tlsSecretAllowed == nil { + return true + } + return b.tlsSecretAllowed(secretName) +} + func (b *Bindings) SetListenerConfiguration(configuration ListenerConfiguration) { b.configure.listener = configuration } @@ -231,12 +248,21 @@ func (b *Bindings) ToBridgeConfig() qdr.BridgeConfig { ListenerAddresses: qdr.ListenerAddressMap{}, } for _, c := range b.connectors { + if c.Spec.TlsCredentials != "" && !b.TlsCredentialIncluded(c.Spec.TlsCredentials) { + continue + } b.configure.connector(b.SiteId, c, &config) } for _, l := range b.listeners { + if l.Spec.TlsCredentials != "" && !b.TlsCredentialIncluded(l.Spec.TlsCredentials) { + continue + } b.configure.listener(b.SiteId, l, &config) } for _, mkl := range b.multiKeyListeners { + if mkl.Spec.TlsCredentials != "" && !b.TlsCredentialIncluded(mkl.Spec.TlsCredentials) { + continue + } b.configure.multiKeyListener(b.SiteId, mkl, &config) } @@ -246,6 +272,9 @@ func (b *Bindings) ToBridgeConfig() qdr.BridgeConfig { func (b *Bindings) AddSslProfiles(config *qdr.RouterConfig) bool { profiles := map[string]qdr.SslProfile{} for _, c := range b.connectors { + if c.Spec.TlsCredentials != "" && !b.TlsCredentialIncluded(c.Spec.TlsCredentials) { + continue + } if c.Spec.TlsCredentials != "" { if !c.Spec.UseClientCert { //if only ca is used, need to qualify the profile to ensure that it does not collide with @@ -262,11 +291,17 @@ func (b *Bindings) AddSslProfiles(config *qdr.RouterConfig) bool { } } for _, l := range b.listeners { + if l.Spec.TlsCredentials != "" && !b.TlsCredentialIncluded(l.Spec.TlsCredentials) { + continue + } if _, ok := profiles[l.Spec.TlsCredentials]; l.Spec.TlsCredentials != "" && !ok { profiles[l.Spec.TlsCredentials] = qdr.ConfigureSslProfile(l.Spec.TlsCredentials, b.ProfilePath, true) } } for _, mkl := range b.multiKeyListeners { + if mkl.Spec.TlsCredentials != "" && !b.TlsCredentialIncluded(mkl.Spec.TlsCredentials) { + continue + } if _, ok := profiles[mkl.Spec.TlsCredentials]; mkl.Spec.TlsCredentials != "" && !ok { profiles[mkl.Spec.TlsCredentials] = qdr.ConfigureSslProfile(mkl.Spec.TlsCredentials, b.ProfilePath, true) } diff --git a/internal/site/routeraccess.go b/internal/site/routeraccess.go index ea312d9ef..027082d61 100644 --- a/internal/site/routeraccess.go +++ b/internal/site/routeraccess.go @@ -60,9 +60,25 @@ func (m RouterAccessMap) findInterRouterRole() (*skupperv2alpha1.RouterAccessRol } func (m RouterAccessMap) DesiredConfig(targetGroups []string, profilePath string) *RouterAccessConfig { + return m.DesiredConfigAllowingTlsSecrets(targetGroups, profilePath, nil) +} + +// DesiredConfigAllowingTlsSecrets is like DesiredConfig but skips RouterAccess entries whose +// spec.tlsCredentials is set when tlsSecretAllowed is non-nil and returns false for that name. +func (m RouterAccessMap) DesiredConfigAllowingTlsSecrets(targetGroups []string, profilePath string, tlsSecretAllowed func(string) bool) *RouterAccessConfig { + source := m + if tlsSecretAllowed != nil { + source = make(RouterAccessMap, len(m)) + for k, ra := range m { + if ra.Spec.TlsCredentials != "" && !tlsSecretAllowed(ra.Spec.TlsCredentials) { + continue + } + source[k] = ra + } + } return &RouterAccessConfig{ - listeners: m.desiredListeners(), - connectors: m.desiredConnectors(targetGroups), + listeners: source.desiredListeners(), + connectors: source.desiredConnectors(targetGroups), profilePath: profilePath, } } diff --git a/internal/site/routeraccess_test.go b/internal/site/routeraccess_test.go index a32b3ac61..e416217b6 100644 --- a/internal/site/routeraccess_test.go +++ b/internal/site/routeraccess_test.go @@ -453,3 +453,27 @@ func TestRouterAccessMap_DesiredConfig(t *testing.T) { }) } } + +func TestRouterAccessMap_DesiredConfigAllowingTlsSecrets(t *testing.T) { + ra := &skupperv2alpha1.RouterAccess{ + ObjectMeta: v1.ObjectMeta{Name: "ra", Namespace: "ns"}, + Spec: skupperv2alpha1.RouterAccessSpec{ + TlsCredentials: "missing-secret", + BindHost: "0.0.0.0", + Roles: []skupperv2alpha1.RouterAccessRole{ + {Name: "inter-router", Port: 55671}, + }, + }, + } + m := RouterAccessMap{"ra": ra} + allow := func(name string) bool { return name != "missing-secret" } + got := m.DesiredConfigAllowingTlsSecrets([]string{"g1"}, "/certs", allow) + if len(got.listeners) != 0 || len(got.connectors) != 0 { + t.Fatalf("expected empty desired when TLS secret disallowed, got listeners=%d connectors=%d", + len(got.listeners), len(got.connectors)) + } + got2 := m.DesiredConfigAllowingTlsSecrets([]string{"g1"}, "/certs", func(string) bool { return true }) + if len(got2.listeners) == 0 { + t.Fatal("expected listeners when TLS secret allowed") + } +}