Skip to content
Draft
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
3 changes: 3 additions & 0 deletions internal/kube/site/attached_connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions internal/kube/site/extended_bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
93 changes: 89 additions & 4 deletions internal/kube/site/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand Down
35 changes: 35 additions & 0 deletions internal/site/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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
Expand All @@ -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)
}
Expand Down
20 changes: 18 additions & 2 deletions internal/site/routeraccess.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down
24 changes: 24 additions & 0 deletions internal/site/routeraccess_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Loading