diff --git a/internal/kube/secrets/manager.go b/internal/kube/secrets/manager.go index 040741861..46d30c5ba 100644 --- a/internal/kube/secrets/manager.go +++ b/internal/kube/secrets/manager.go @@ -241,6 +241,21 @@ func (w *ProfilesWatcher) keyfunc(name string) string { return w.namespace + "/" + name } +// TlsCredentialSecretPresent reports whether a TLS secret for the credential is in the +// ProfilesWatcher informer cache (same source kube-adaptor uses for ssl profiles). +func (w *ProfilesWatcher) TlsCredentialSecretPresent(credentialName string) bool { + if credentialName == "" || w.Cache == nil { + return false + } + for _, secretName := range profileSecrets(credentialName) { + secret, err := w.Cache.Get(w.keyfunc(secretName)) + if err == nil && secret != nil && secret.Type == corev1.SecretTypeTLS { + return true + } + } + return false +} + func (w *ProfilesWatcher) checkPriorValidity(secret *corev1.Secret) uint64 { result := w.pvProvider.TLSPriorValidRevisions() if secret.ObjectMeta.Annotations == nil { diff --git a/internal/kube/site/attached_connector.go b/internal/kube/site/attached_connector.go index bbe7b7bbc..1d7aa4016 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.IsTlsSecretPresent(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..ed95f3df9 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.IsTlsSecretPresent(ptl.definition.Spec.TlsCredentials) { + continue + } if ptl.updateBridgeConfig(b.bindings.SiteId, &desired) { updated = true } @@ -426,6 +429,14 @@ func (b *ExtendedBindings) AddSslProfiles(config *qdr.RouterConfig, definitions profiles := map[string]qdr.SslProfile{} for _, c := range definitions { if c.Spec.TlsCredentials != "" { + if !b.bindings.IsTlsSecretPresent(c.Spec.TlsCredentials) { + b.logger.Info("Skipping attached connector TLS profile until credentials secret exists", + slog.String("namespace", c.Namespace), + slog.String("name", c.Name), + slog.String("secret", c.Spec.TlsCredentials), + ) + continue + } if !c.Spec.UseClientCert { //if only ca is used, need to qualify the profile to ensure that it does not collide with // use of the same secret where client auth *is* required @@ -451,6 +462,7 @@ func (b *ExtendedBindings) AddSslProfiles(config *qdr.RouterConfig, definitions func (b *ExtendedBindings) SetSite(site *Site) { b.bindings.SetSiteId(site.site.GetSiteId()) + b.bindings.SetIsTlsSecretPresent(site.tlsCredentialSecretPresent) b.site = site } @@ -527,6 +539,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.IsTlsSecretPresent(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..e8f8078ba 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,79 @@ func (s *Site) ownerReferences() []metav1.OwnerReference { } } +// tlsCredentialSecretPresent reports whether a TLS credential secret is visible in the +// ProfilesWatcher cache (not a live API GET). +func (s *Site) tlsCredentialSecretPresent(secretName string) bool { + if s.profiles == nil { + return false + } + return s.profiles.TlsCredentialSecretPresent(secretName) +} + +func (s *Site) missingTlsCredentialsErr(tlsCredentials string) error { + if tlsCredentials == "" || s.tlsCredentialSecretPresent(tlsCredentials) { + return nil + } + return fmt.Errorf("TLS credentials secret %q not found", tlsCredentials) +} + +// 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.DesiredConfigWithAvailableCredentials(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 +843,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.DesiredConfigWithAvailableCredentials(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 +856,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.DesiredConfigWithAvailableCredentials(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), @@ -934,21 +1007,38 @@ func (s *Site) updateConnectorConfiguredStatusWithSelectedPods(connector *skuppe } func (s *Site) CheckConnector(name string, connector *skupperv2alpha1.Connector) error { - update := s.bindings.UpdateConnector(name, connector) if s.site == nil { if connector == nil { return nil } return s.updateConnectorConfiguredStatus(connector, stderrors.New("No active site in namespace")) } - if update == nil { - return nil + var tlsErr error + if connector != nil { + tlsErr = s.missingTlsCredentialsErr(connector.Spec.TlsCredentials) + if tlsErr != nil { + s.logger.Info("Deferring connector router configuration until TLS credentials secret exists", + slog.String("namespace", s.namespace), + slog.String("connector", connector.Name), + slog.String("secret", connector.Spec.TlsCredentials), + ) + } } - err := s.updateRouterConfig(update) + update := s.bindings.UpdateConnector(name, connector) if connector == nil { - return err + if update != nil { + return s.updateRouterConfig(update) + } + return nil + } + if update == nil { + if tlsErr != nil { + return s.updateConnectorConfiguredStatus(connector, tlsErr) + } + return nil } - return s.updateConnectorConfiguredStatus(connector, err) + routerErr := s.updateRouterConfig(update) + return s.updateConnectorConfiguredStatus(connector, stderrors.Join(tlsErr, routerErr)) } func (s *Site) updateListenerStatus(listener *skupperv2alpha1.Listener, err error) error { @@ -1040,15 +1130,32 @@ func (s *Site) CheckListener(name string, listener *skupperv2alpha1.Listener, sv } } } + var tlsErr error + if listener != nil { + tlsErr = s.missingTlsCredentialsErr(listener.Spec.TlsCredentials) + if tlsErr != nil { + s.logger.Info("Deferring listener router configuration until TLS credentials secret exists", + slog.String("namespace", s.namespace), + slog.String("listener", listener.Name), + slog.String("secret", listener.Spec.TlsCredentials), + ) + } + } update, err1 := s.bindings.UpdateListener(name, listener) + if listener == nil { + if update == nil { + return nil + } + return stderrors.Join(err1, s.updateRouterConfig(update)) + } if update == nil { + if tlsErr != nil { + return s.updateListenerStatus(listener, tlsErr) + } return nil } err2 := s.updateRouterConfig(update) - if listener == nil { - return stderrors.Join(err1, err2) - } - return s.updateListenerStatus(listener, stderrors.Join(err1, err2)) + return s.updateListenerStatus(listener, stderrors.Join(tlsErr, err1, err2)) } func (s *Site) CheckMultiKeyListener(name string, mkl *skupperv2alpha1.MultiKeyListener) error { @@ -1176,6 +1283,14 @@ func (s *Site) link(linkconfig *skupperv2alpha1.Link) error { config.UpdateProxyConfig(currentProxyConfig) } } + if tlsErr := s.missingTlsCredentialsErr(linkconfig.Spec.TlsCredentials); tlsErr != nil { + 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, tlsErr) + } err := s.updateRouterConfig(config) return s.updateLinkConfiguredCondition(linkconfig, err) } else { @@ -1531,12 +1646,23 @@ func (s *Site) CheckRouterAccess(name string, la *skupperv2alpha1.RouterAccess) if !s.initialised { return nil } + var configuredErr error + if la != nil { + configuredErr = s.missingTlsCredentialsErr(la.Spec.TlsCredentials) + } + if configuredErr != nil { + s.logger.Info("Deferring RouterAccess router configuration until TLS credentials secret exists", + slog.String("namespace", s.namespace), + slog.String("routerAccess", la.Name), + slog.String("secret", la.Spec.TlsCredentials), + ) + } var previousGroups []string groups := s.groups() 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.DesiredConfigWithAvailableCredentials(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)) @@ -1568,6 +1694,9 @@ func (s *Site) CheckRouterAccess(name string, la *skupperv2alpha1.RouterAccess) if len(errors) > 0 { err = fmt.Errorf("%s", strings.Join(errors, ", ")) } + if configuredErr != nil { + err = stderrors.Join(configuredErr, err) + } if la != nil && la.SetConfigured(err) { if err := s.updateRouterAccessStatus(la); err != nil { return err diff --git a/internal/site/bindings.go b/internal/site/bindings.go index 72da8de09..45d16ca11 100644 --- a/internal/site/bindings.go +++ b/internal/site/bindings.go @@ -23,13 +23,14 @@ type ListenerFunction func(*skupperv2alpha1.Listener) *skupperv2alpha1.Listener type MultiKeyListenerFunction func(*skupperv2alpha1.MultiKeyListener) *skupperv2alpha1.MultiKeyListener type Bindings struct { - SiteId string - ProfilePath string - connectors map[string]*skupperv2alpha1.Connector - listeners map[string]*skupperv2alpha1.Listener - multiKeyListeners map[string]*skupperv2alpha1.MultiKeyListener - handler BindingEventHandler - configure struct { + SiteId string + ProfilePath string + connectors map[string]*skupperv2alpha1.Connector + listeners map[string]*skupperv2alpha1.Listener + multiKeyListeners map[string]*skupperv2alpha1.MultiKeyListener + handler BindingEventHandler + isTlsSecretPresent func(secretName string) bool + configure struct { listener ListenerConfiguration connector ConnectorConfiguration multiKeyListener MultiKeyListenerConfiguration @@ -53,6 +54,22 @@ func (b *Bindings) SetSiteId(siteId string) { b.SiteId = siteId } +func (b *Bindings) SetIsTlsSecretPresent(f func(string) bool) { + b.isTlsSecretPresent = f +} + +// IsTlsSecretPresent reports whether TLS material for the given secret name is available +// (e.g. in the ProfilesWatcher cache). Empty name always returns true. +func (b *Bindings) IsTlsSecretPresent(secretName string) bool { + if secretName == "" { + return true + } + if b.isTlsSecretPresent == nil { + return true + } + return b.isTlsSecretPresent(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.IsTlsSecretPresent(c.Spec.TlsCredentials) { + continue + } b.configure.connector(b.SiteId, c, &config) } for _, l := range b.listeners { + if l.Spec.TlsCredentials != "" && !b.IsTlsSecretPresent(l.Spec.TlsCredentials) { + continue + } b.configure.listener(b.SiteId, l, &config) } for _, mkl := range b.multiKeyListeners { + if mkl.Spec.TlsCredentials != "" && !b.IsTlsSecretPresent(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.IsTlsSecretPresent(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.IsTlsSecretPresent(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.IsTlsSecretPresent(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..8b44463d5 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.DesiredConfigWithAvailableCredentials(targetGroups, profilePath, nil) +} + +// DesiredConfigWithAvailableCredentials is like DesiredConfig but skips RouterAccess entries whose +// spec.tlsCredentials is set when isTlsSecretPresent is non-nil and returns false for that name. +func (m RouterAccessMap) DesiredConfigWithAvailableCredentials(targetGroups []string, profilePath string, isTlsSecretPresent func(string) bool) *RouterAccessConfig { + source := m + if isTlsSecretPresent != nil { + source = make(RouterAccessMap, len(m)) + for k, ra := range m { + if ra.Spec.TlsCredentials != "" && !isTlsSecretPresent(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..bfd0fc492 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_DesiredConfigWithAvailableCredentials(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.DesiredConfigWithAvailableCredentials([]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.DesiredConfigWithAvailableCredentials([]string{"g1"}, "/certs", func(string) bool { return true }) + if len(got2.listeners) == 0 { + t.Fatal("expected listeners when TLS secret allowed") + } +}