diff --git a/internal/kube/adaptor/config_init.go b/internal/kube/adaptor/config_init.go index be662c9bb..1d20e5034 100644 --- a/internal/kube/adaptor/config_init.go +++ b/internal/kube/adaptor/config_init.go @@ -48,13 +48,20 @@ func InitialiseConfig(cli internalclient.Clients, namespace string, path string, if routerConfiguration == nil { return fmt.Errorf("empty router configuration in ConfigMap %q", routerConfigMap) } - delta := secretsSync.Expect(routerConfiguration.SslProfiles) + delta := secretsSync.ExpectSslProfiles(routerConfiguration.SslProfiles) if len(delta.Missing) > 0 { slog.Info("Waiting for Secrets to be created for SslProfiles", slog.Any("sslProfiles", delta.Missing)) } for name, diff := range delta.PendingOrdinals { slog.Info("Secret has outdated ordinal", slog.String("secret", diff.SecretName), slog.Uint64("ordinal", diff.Current), slog.String("profile", name), slog.Uint64("expected", diff.Expect)) } + deltaProxy := secretsSync.ExpectProxyProfiles(namespace+"/"+routerConfigMap, routerConfiguration.ProxyProfiles) + if len(deltaProxy.Missing) > 0 { + slog.Info("Waiting for Secrets to be created for ProxyProfiles", slog.Any("proxProfiles", deltaProxy.Missing)) + } + for _, err := range deltaProxy.Errors { + delta.Errors = append(delta.Errors, err) + } return delta.Error() }, backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(time.Second*60))) if retryErr != nil { diff --git a/internal/kube/adaptor/config_sync.go b/internal/kube/adaptor/config_sync.go index ccbc523e5..2aadbabd3 100644 --- a/internal/kube/adaptor/config_sync.go +++ b/internal/kube/adaptor/config_sync.go @@ -14,7 +14,8 @@ import ( ) // Syncs the live router config with the configmap (bridge configuration, -// secrets for services with TLS enabled, and secrets and connectors for links) +// secrets for services with TLS enabled, and secrets and connectors for links +// as well as proxy profiles) type ConfigSync struct { agentPool *qdr.AgentPool controller *watchers.EventProcessor @@ -102,6 +103,12 @@ func (c *ConfigSync) configEvent(key string, configmap *corev1.ConfigMap) error if err := c.syncSslProfilesToRouter(desired.SslProfiles); err != nil { return err } + if err := c.syncProxyProfileCredentialsToDisk(key, desired.ProxyProfiles); err != nil { + return err + } + if err := c.syncProxyProfilesToRouter(desired.ProxyProfiles); err != nil { + return err + } if err := c.syncBridgeConfig(&desired.Bridges); err != nil { c.logger.Error("sync failed", slog.Any("error", err)) return err @@ -110,6 +117,7 @@ func (c *ConfigSync) configEvent(key string, configmap *corev1.ConfigMap) error c.logger.Error("sync failed", slog.Any("error", err)) return err } + return nil } @@ -219,6 +227,7 @@ func (c *ConfigSync) syncSslProfilesToRouter(desired map[string]qdr.SslProfile) if err := agent.CreateSslProfile(profile); err != nil { return err } + continue } if current != profile { if err := agent.UpdateSslProfile(profile); err != nil { @@ -237,7 +246,47 @@ func (c *ConfigSync) syncSslProfilesToRouter(desired map[string]qdr.SslProfile) } func (c *ConfigSync) syncSslProfileCredentialsToDisk(profiles map[string]qdr.SslProfile) error { - delta := c.profileSyncer.Expect(profiles) + delta := c.profileSyncer.ExpectSslProfiles(profiles) + return delta.Error() +} + +func (c *ConfigSync) syncProxyProfilesToRouter(desired map[string]qdr.ProxyProfile) error { + agent, err := c.agentPool.Get() + if err != nil { + return err + } + defer c.agentPool.Put(agent) + actual, err := agent.GetProxyProfiles() + if err != nil { + return err + } + + for _, profile := range desired { + current, ok := actual[profile.Name] + if !ok { + if err := agent.CreateProxyProfile(profile); err != nil { + return err + } + continue + } + if current != profile { + if err := agent.UpdateProxyProfile(profile); err != nil { + return err + } + } + } + for _, profile := range actual { + if _, ok := desired[profile.Name]; !ok { + if err := agent.Delete("io.skupper.router.proxyProfile", profile.Name); err != nil { + return err + } + } + } + return nil +} + +func (c *ConfigSync) syncProxyProfileCredentialsToDisk(key string, profiles map[string]qdr.ProxyProfile) error { + delta := c.profileSyncer.ExpectProxyProfiles(key, profiles) return delta.Error() } diff --git a/internal/kube/controller/controller.go b/internal/kube/controller/controller.go index d6ef2895d..f484fa40d 100644 --- a/internal/kube/controller/controller.go +++ b/internal/kube/controller/controller.go @@ -560,7 +560,7 @@ func (c *Controller) routerConfigUpdate(_ string, cm *corev1.ConfigMap) error { if err != nil { return err } - c.getSite(cm.Namespace).CheckSslProfiles(config) + c.getSite(cm.Namespace).CheckSslAndProxyProfiles(config) return nil } diff --git a/internal/kube/secrets/manager.go b/internal/kube/secrets/manager.go index 03f80f358..040741861 100644 --- a/internal/kube/secrets/manager.go +++ b/internal/kube/secrets/manager.go @@ -36,7 +36,7 @@ type profileWatcherContext struct { type ProfilesWatcher struct { logger *slog.Logger - cache SecretsCache + Cache SecretsCache client typedv1.SecretInterface update UpdateRouterConfigFn pvProvider PriorValidityProvider @@ -58,7 +58,7 @@ func NewProfilesWatcher(factory SecretsCacheFactory, client kubernetes.Interface state: make(map[string]*profileWatcherContext), cleanup: sync.OnceFunc(func() { close(stopCh) }), } - w.cache = factory(stopCh, w.handleSecret) + w.Cache = factory(stopCh, w.handleSecret) return w } @@ -72,106 +72,162 @@ func (w *ProfilesWatcher) handleSecret(key string, secret *corev1.Secret) error } secretName := secret.ObjectMeta.Name changed := false - var secretsContext profileContextSet - for _, profileName := range secretProfiles(secretName) { - state, ok := w.state[profileName] - if !ok { - continue + switch secret.Type { + case "kubernetes.io/tls": + var secretsContext profileContextSet + for _, profileName := range secretProfiles(secretName) { + state, ok := w.state[profileName] + if !ok { + continue + } + if state.SecretKey == "" { + state.SecretKey = key + updateSecretChecksum(secret, &state.SecretContentSum) + } else if state.SecretKey != key { + continue + } + if updateSecretChecksum(secret, &state.SecretContentSum) { + state.Ordinal += 1 + changed = true + } + pv := w.checkPriorValidity(secret) + nextOldest := state.Ordinal - pv + if pv <= state.Ordinal && nextOldest > state.OldestValidOrdinal { + changed = true + state.OldestValidOrdinal = nextOldest + } + secretsContext = append(secretsContext, profileContext{ + ProfileName: profileName, + Ordinal: state.Ordinal, + }) } - if state.SecretKey == "" { - state.SecretKey = key - updateSecretChecksum(secret, &state.SecretContentSum) - } else if state.SecretKey != key { - continue + updated, err := updateSecret(secret, secretsContext) + if err != nil { + return err } - if updateSecretChecksum(secret, &state.SecretContentSum) { - state.Ordinal += 1 - changed = true + if updated { + w.logger.Debug("Updating ssl-profile-ordinal secret", slog.String("secret", secretName), slog.Any("context", secretsContext)) + if _, err := w.client.Update(context.TODO(), secret, v1.UpdateOptions{}); err != nil { + return fmt.Errorf("error updating sslProfile secret anntations: %s", err) + } } - pv := w.checkPriorValidity(secret) - nextOldest := state.Ordinal - pv - if pv <= state.Ordinal && nextOldest > state.OldestValidOrdinal { - changed = true - state.OldestValidOrdinal = nextOldest + if !changed { + return nil } - secretsContext = append(secretsContext, profileContext{ - ProfileName: profileName, - Ordinal: state.Ordinal, - }) - } - updated, err := updateSecret(secret, secretsContext) - if err != nil { - return err - } - if updated { - w.logger.Debug("Updating ssl-profile-ordinal secret", slog.String("secret", secretName), slog.Any("context", secretsContext)) - if _, err := w.client.Update(context.TODO(), secret, v1.UpdateOptions{}); err != nil { - return fmt.Errorf("error updating sslProfile secret anntations: %s", err) + w.logger.Info("SslProfile Secret Changed", + slog.String("name", secretName), + slog.Any("context", secretsContext), + ) + return w.update(w) + case "kubernetes.io/basic-auth": + state, ok := w.state[secretName] + if ok { + if state.SecretKey == "" { + state.SecretKey = key + updateSecretChecksum(secret, &state.SecretContentSum) + } else if state.SecretKey == key { + if updateSecretChecksum(secret, &state.SecretContentSum) { + changed = true + } + } } + if !changed { + return nil + } + w.logger.Info("ProxyProfile Secret Changed", + slog.String("name", secretName), + ) + return w.update(w) } - if !changed { - return nil - } - w.logger.Info("SslProfile Secret Changed", - slog.String("name", secretName), - slog.Any("context", secretsContext), - ) - return w.update(w) + return nil } func (w *ProfilesWatcher) Apply(config *qdr.RouterConfig) bool { changed := false - for profileName, configured := range config.SslProfiles { - state, ok := w.state[profileName] + for sslProfileName, configured := range config.SslProfiles { + state, ok := w.state[sslProfileName] if !ok { continue } if configured.Ordinal != state.Ordinal { changed = true configured.Ordinal = state.Ordinal - config.SslProfiles[profileName] = configured + config.SslProfiles[sslProfileName] = configured } if configured.OldestValidOrdinal != state.OldestValidOrdinal { changed = true configured.OldestValidOrdinal = state.OldestValidOrdinal - config.SslProfiles[profileName] = configured + config.SslProfiles[sslProfileName] = configured + } + } + for proxyProfileName, configured := range config.ProxyProfiles { + _, ok := w.state[proxyProfileName] + if !ok { + continue + } + key := w.keyfunc(proxyProfileName) + secret, err := w.Cache.Get(key) + if err != nil || secret == nil { + continue } + configured.Host = string(secret.Data["host"]) + configured.Port = string(secret.Data["port"]) + configured.Username = string(secret.Data["username"]) + config.ProxyProfiles[proxyProfileName] = configured + changed = true } return changed } -func (w *ProfilesWatcher) UseProfiles(profiles map[string]qdr.SslProfile) { +func (w *ProfilesWatcher) UseProfiles(sslProfiles map[string]qdr.SslProfile, proxyProfiles map[string]qdr.ProxyProfile) { found := make(map[string]struct{}, len(w.state)) for profileName := range w.state { found[profileName] = struct{}{} } - for profileName, config := range profiles { - delete(found, profileName) - state, ok := w.state[profileName] + for sslProfileName, config := range sslProfiles { + delete(found, sslProfileName) + state, ok := w.state[sslProfileName] if !ok { state = &profileWatcherContext{ Ordinal: config.Ordinal, OldestValidOrdinal: config.OldestValidOrdinal, } - w.state[profileName] = state + w.state[sslProfileName] = state } if state.SecretKey != "" { continue } - for _, secretName := range profileSecrets(profileName) { + for _, secretName := range profileSecrets(sslProfileName) { key := w.keyfunc(secretName) - secret, err := w.cache.Get(key) + secret, err := w.Cache.Get(key) if err != nil || secret == nil { continue } w.handleSecret(key, secret) } } + for proxyProfileName := range proxyProfiles { + delete(found, proxyProfileName) + state, ok := w.state[proxyProfileName] + if !ok { + state = &profileWatcherContext{} + w.state[proxyProfileName] = state + } + if state.SecretKey != "" { + continue + } + key := w.keyfunc(proxyProfileName) + secret, err := w.Cache.Get(key) + if err != nil || secret == nil { + continue + } + w.handleSecret(key, secret) + } for profileName := range found { state := w.state[profileName] delete(w.state, profileName) if state != nil && state.SecretKey != "" { - secret, err := w.cache.Get(state.SecretKey) + secret, err := w.Cache.Get(state.SecretKey) if err != nil || secret == nil { continue } diff --git a/internal/kube/secrets/sync.go b/internal/kube/secrets/sync.go index edff8ad25..9ae994555 100644 --- a/internal/kube/secrets/sync.go +++ b/internal/kube/secrets/sync.go @@ -10,6 +10,7 @@ import ( "github.com/skupperproject/skupper/internal/qdr" corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/cache" ) type syncContext struct { @@ -25,9 +26,10 @@ type Sync struct { cache SecretsCache callback Callback - mu sync.Mutex - configured map[string]qdr.SslProfile - profileSecrets map[string]syncContext + mu sync.Mutex + configuredSsl map[string]qdr.SslProfile + configuredProxy map[string]qdr.ProxyProfile + profileSecrets map[string]syncContext cleanup func() } @@ -35,11 +37,12 @@ type Sync struct { func NewSync(factory SecretsCacheFactory, callback Callback, logger *slog.Logger) *Sync { stopCh := make(chan struct{}) sync := &Sync{ - cleanup: sync.OnceFunc(func() { close(stopCh) }), - logger: logger, - callback: callback, - configured: make(map[string]qdr.SslProfile), - profileSecrets: make(map[string]syncContext), + cleanup: sync.OnceFunc(func() { close(stopCh) }), + logger: logger, + callback: callback, + configuredSsl: make(map[string]qdr.SslProfile), + configuredProxy: make(map[string]qdr.ProxyProfile), + profileSecrets: make(map[string]syncContext), } sync.cache = factory(stopCh, sync.handle) return sync @@ -65,9 +68,9 @@ func (s *Sync) Recover() { } } -func (s *Sync) handleProfile(key string, secret *corev1.Secret, pctx profileContext) (bool, error) { +func (s *Sync) handleSslProfile(key string, secret *corev1.Secret, pctx profileContext) (bool, error) { prev, hadPrev := s.getProfile(pctx.ProfileName) - configuredProfile, isConfigured := s.getConfigured(pctx.ProfileName) + configuredProfile, isConfigured := s.getConfiguredSsl(pctx.ProfileName) updated := syncContext{ profileContext: pctx, SecretKey: key, @@ -117,27 +120,74 @@ func (s *Sync) handleProfile(key string, secret *corev1.Secret, pctx profileCont return viableUpdate, nil } -func (s *Sync) handle(key string, secret *corev1.Secret) error { - if secret == nil { - return nil +func (s *Sync) handleProxyProfile(namespace string, secret *corev1.Secret) (bool, error) { + profileName := secret.Name + prev, hadPrev := s.getProfile(profileName) + proxyProfile, isConfigured := s.getConfiguredProxy(profileName) + updated := syncContext{ + profileContext: profileContext{ + ProfileName: profileName, + }, + SecretKey: namespace + "/" + profileName, } - metadata, found, err := fromSecret(secret) - if err != nil { - return fmt.Errorf("failed to decode secret metadata: %s", err) + if !isConfigured { + s.setProfileSecret(updated) + return false, nil + } + sumChanged := updateSecretChecksum(secret, &prev.SecretContentSum) + updated.SecretContentSum = prev.SecretContentSum + hasWrite := false + if !hadPrev || sumChanged { + if len(secret.Data["username"]) > 0 && len(secret.Data["password"]) > 0 { + path := strings.TrimPrefix(proxyProfile.Password, "file:") + if err := writeProxyProfile(secret, path); err != nil { + return false, fmt.Errorf("write for proxyProfile failed: %s", err) + } + hasWrite = true + } } - if !found { + s.setProfileSecret(updated) + return hasWrite, nil +} + +func (s *Sync) handle(key string, secret *corev1.Secret) error { + if secret == nil { return nil } - for _, profileMetadata := range metadata { - profileName := profileMetadata.ProfileName - updated, err := s.handleProfile(key, secret, profileMetadata) + + switch secret.Type { + case "kubernetes.io/tls": + metadata, found, err := fromSecret(secret) + if err != nil { + return fmt.Errorf("failed to decode secret metadata: %s", err) + } + if !found { + return nil + } + for _, profileMetadata := range metadata { + profileName := profileMetadata.ProfileName + updated, err := s.handleSslProfile(key, secret, profileMetadata) + if err != nil { + return fmt.Errorf("error handling secret %q for profile %s: %s", key, profileName, err) + } + if updated { + s.doCallback(profileName) + } + } + case "kubernetes.io/basic-auth": + namespace, _, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return err + } + updated, err := s.handleProxyProfile(namespace, secret) if err != nil { - return fmt.Errorf("error handling secret %q for profile %s: %s", key, profileName, err) + return fmt.Errorf("error handling secret %q for proxy profile: %s", key, err) } if updated { - s.doCallback(profileName) + s.doCallback(secret.Name) } } + return nil } @@ -152,21 +202,21 @@ func (s *Sync) setProfileSecret(pctx syncContext) { defer s.mu.Unlock() s.profileSecrets[pctx.ProfileName] = pctx } -func (s *Sync) getConfigured(profileName string) (qdr.SslProfile, bool) { +func (s *Sync) getConfiguredSsl(profileName string) (qdr.SslProfile, bool) { s.mu.Lock() defer s.mu.Unlock() - result, ok := s.configured[profileName] + result, ok := s.configuredSsl[profileName] return result, ok } -func (s *Sync) setConfigured(profiles map[string]qdr.SslProfile) { +func (s *Sync) setConfiguredSsl(profiles map[string]qdr.SslProfile) { s.mu.Lock() defer s.mu.Unlock() - s.configured = profiles + s.configuredSsl = profiles } -func (s *Sync) Expect(profiles map[string]qdr.SslProfile) SyncDelta { +func (s *Sync) ExpectSslProfiles(profiles map[string]qdr.SslProfile) SyncDelta { var delta SyncDelta - s.setConfigured(profiles) + s.setConfiguredSsl(profiles) for profileName, qdrProfile := range profiles { context, ok := s.getProfile(profileName) if !ok { @@ -185,7 +235,7 @@ func (s *Sync) Expect(profiles map[string]qdr.SslProfile) SyncDelta { } else { secret, _ := s.cache.Get(context.SecretKey) if secret != nil { - _, err := s.handleProfile(context.SecretKey, secret, context.profileContext) + _, err := s.handleSslProfile(context.SecretKey, secret, context.profileContext) if err != nil { delta.Errors = append(delta.Errors, err) } @@ -195,6 +245,46 @@ func (s *Sync) Expect(profiles map[string]qdr.SslProfile) SyncDelta { return delta } +func (s *Sync) getConfiguredProxy(profileName string) (qdr.ProxyProfile, bool) { + s.mu.Lock() + defer s.mu.Unlock() + result, ok := s.configuredProxy[profileName] + return result, ok +} +func (s *Sync) setConfiguredProxy(profiles map[string]qdr.ProxyProfile) { + s.mu.Lock() + defer s.mu.Unlock() + s.configuredProxy = profiles +} + +func (s *Sync) ExpectProxyProfiles(key string, profiles map[string]qdr.ProxyProfile) SyncDelta { + var delta SyncDelta + delta.ProxyUpdates = make(map[string]qdr.ProxyProfile) + s.setConfiguredProxy(profiles) + namespace, _, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + delta.Errors = append(delta.Errors, err) + } + for profileName, profile := range profiles { + secret, _ := s.cache.Get(namespace + "/" + profileName) + if secret == nil { + delta.Missing = append(delta.Missing, profileName) + continue + } else { + _, err := s.handleProxyProfile(namespace, secret) + if err != nil { + delta.Errors = append(delta.Errors, err) + } + profile.Host = string(secret.Data["host"]) + profile.Port = string(secret.Data["port"]) + profile.Username = string(secret.Data["username"]) + delta.ProxyUpdates[profileName] = profile + } + + } + return delta +} + type OrdinalDelta struct { SecretName string Expect uint64 @@ -203,6 +293,7 @@ type OrdinalDelta struct { type SyncDelta struct { Missing []string PendingOrdinals map[string]OrdinalDelta + ProxyUpdates map[string]qdr.ProxyProfile Errors []error } @@ -254,6 +345,21 @@ func writeSslProfile(secret *corev1.Secret, profile qdr.SslProfile) error { return nil } +func writeProxyProfile(secret *corev1.Secret, filePath string) error { + _, ok := secret.Data["password"] + if !ok { + return fmt.Errorf("empty proxyProfile %q", secret.Name) + } + baseName := path.Dir(filePath) + if err := os.MkdirAll(baseName, 0755); err != nil { + return fmt.Errorf("error making proxyProfile password directory %q: %e", baseName, err) + } + if err := writeFile(filePath, []byte(secret.Data["password"]), 0644); err != nil { + return fmt.Errorf("error writing password.txt: %e", err) + } + return nil +} + func writeFile(path string, data []byte, perm os.FileMode) error { if path == "" { return nil diff --git a/internal/kube/secrets/sync_test.go b/internal/kube/secrets/sync_test.go index f2ca3641a..8715a3cfc 100644 --- a/internal/kube/secrets/sync_test.go +++ b/internal/kube/secrets/sync_test.go @@ -18,9 +18,10 @@ const tlsProfKey = `internal.skupper.io/tls-profile-context` var ( expectedFiles = []string{"ca.crt", "tls.crt", "tls.key"} expectedCAOnlyFiles = []string{"ca.crt"} + expectedProxyFiles = []string{"password.txt"} ) -func TestSyncExpect(t *testing.T) { +func TestSyncExpectSsl(t *testing.T) { tmpdir := t.TempDir() tlog := slog.New(slog.NewTextHandler(io.Discard, nil)) sCache := secretsCacheFactoryFixture(t, "testing") @@ -29,7 +30,7 @@ func TestSyncExpect(t *testing.T) { secretSync := secrets.NewSync(sCache.Factory, nil, tlog) secretSync.Recover() - delta := secretSync.Expect(map[string]qdr.SslProfile{ + delta := secretSync.ExpectSslProfiles(map[string]qdr.SslProfile{ "test-tls": fixtureSslProfile("test-tls", tmpdir, 0, 0, false), "test-tls-profile": fixtureSslProfile("test-tls-profile", tmpdir, 8, 0, true), }) @@ -40,6 +41,25 @@ func TestSyncExpect(t *testing.T) { assertFiles(t, path.Join(tmpdir, "test-tls-profile"), expectedCAOnlyFiles) } +func TestSyncExpectProxy(t *testing.T) { + tmpdir := t.TempDir() + tlog := slog.New(slog.NewTextHandler(io.Discard, nil)) + sCache := secretsCacheFactoryFixture(t, "testing") + sCache.Secrets["testing/test-proxy"] = fixtureProxySecret("test-proxy", "testing", "8888", "Barney", "Rubble") + sCache.Secrets["testing/test-proxy-no-auth"] = fixtureProxySecret("test-proxy-no-auth", "testing", "8888", "", "") + + secretSync := secrets.NewSync(sCache.Factory, nil, tlog) + secretSync.Recover() + delta := secretSync.ExpectProxyProfiles("testing/test-proxy", map[string]qdr.ProxyProfile{ + "test-proxy": fixtureProxyProfile("test-proxy", "proxy-service.testing.svc.cluster.local", "8888", "Barney", tmpdir), + "test-proxy-no-auth": fixtureProxyProfile("test-proxy-no-auth", "proxy-service.testing.svc.cluster.local", "8888", "", ""), + }) + if !delta.Empty() { + t.Errorf("expected all profiles to be resolved by secret: %s", delta.Error()) + } + assertFiles(t, path.Join(tmpdir, "test-proxy"), expectedProxyFiles) +} + func TestSyncHandler(t *testing.T) { tmpdir := t.TempDir() tlog := slog.New(slog.NewTextHandler(io.Discard, nil)) @@ -50,7 +70,7 @@ func TestSyncHandler(t *testing.T) { configuredProfiles := map[string]qdr.SslProfile{ "test-tls": fixtureSslProfile("test-tls", tmpdir, 12, 11, false), } - delta := secretSync.Expect(configuredProfiles) + delta := secretSync.ExpectSslProfiles(configuredProfiles) if len(delta.Missing) != 1 { t.Errorf("expected missing profile test-tls: %s", delta.Error()) } @@ -64,7 +84,7 @@ func TestSyncHandler(t *testing.T) { t.Errorf("unexpected error handling new secret: %s", err) } - delta = secretSync.Expect(configuredProfiles) + delta = secretSync.ExpectSslProfiles(configuredProfiles) diff := delta.PendingOrdinals["test-tls"] expectDiff := secrets.OrdinalDelta{Expect: 12, Current: 1, SecretName: "testing/test-tls"} if diff != expectDiff { @@ -78,7 +98,7 @@ func TestSyncHandler(t *testing.T) { if err := sCache.HandlerFn("testing/test-tls", sCache.Secrets["testing/test-tls"]); err != nil { t.Errorf("unexpected error handling updated secret: %s", err) } - delta = secretSync.Expect(configuredProfiles) + delta = secretSync.ExpectSslProfiles(configuredProfiles) if !delta.Empty() { t.Errorf("expected all profiles to be resolved: %s", delta.Error()) } @@ -99,7 +119,7 @@ func TestSyncCallback(t *testing.T) { configuredProfiles := map[string]qdr.SslProfile{ "test-tls": fixtureSslProfile("test-tls", tmpdir, 12, 11, false), } - delta := secretSync.Expect(configuredProfiles) + delta := secretSync.ExpectSslProfiles(configuredProfiles) if delta.Empty() { t.Errorf("expected missing secret") } @@ -118,7 +138,7 @@ func TestSyncCallback(t *testing.T) { if len(callbackValues) != 1 || callbackValues[0] != "test-tls" { t.Errorf("expected one callback for test-tls profile: %s", callbackValues) } - delta = secretSync.Expect(configuredProfiles) + delta = secretSync.ExpectSslProfiles(configuredProfiles) if !delta.Empty() { t.Errorf("expected all profiles to be resolved: %s", delta.Error()) } @@ -160,6 +180,24 @@ func fixtureTlsSecret(name, namespace string) *corev1.Secret { "tls.crt": []byte("tls.crt - " + name), "tls.key": []byte("tls.key - " + name), }, + Type: "kubernetes.io/tls", + } +} + +func fixtureProxySecret(name, namespace string, port string, username string, password string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{}, + }, + Data: map[string][]byte{ + "host": []byte("proxy-service." + namespace + ".svc.cluster.local"), + "port": []byte(port), + "username": []byte(username), + "password": []byte(password), + }, + Type: "kubernetes.io/basic-auth", } } @@ -177,6 +215,20 @@ func fixtureSslProfile(name, baseDir string, ord, oldestOrd uint64, caonly bool) return profile } +func fixtureProxyProfile(name string, host string, port string, username string, filepath string) qdr.ProxyProfile { + profile := qdr.ProxyProfile{ + Name: name, + Host: host, + Port: port, + Password: filepath, + } + if username != "" { + profile.Username = username + profile.Password = path.Join("file:", filepath, name, "password.txt") + } + return profile +} + func secretsCacheFactoryFixture(t *testing.T, ns string) *stubSecretsCache { t.Helper() return &stubSecretsCache{ diff --git a/internal/kube/site/resources/skupper-router-deployment.yaml b/internal/kube/site/resources/skupper-router-deployment.yaml index 994ad8025..a132cd473 100644 --- a/internal/kube/site/resources/skupper-router-deployment.yaml +++ b/internal/kube/site/resources/skupper-router-deployment.yaml @@ -114,6 +114,8 @@ spec: volumeMounts: - mountPath: /etc/skupper-router-certs name: skupper-router-certs + - mountPath: /etc/skupper-router-proxies + name: skupper-router-proxies {{- if .Sizing.Router.NotEmpty -}} {{- template "resources" .Sizing.Router -}} {{- end }} @@ -159,6 +161,8 @@ spec: volumeMounts: - mountPath: /etc/skupper-router-certs name: skupper-router-certs + - mountPath: /etc/skupper-router-proxies + name: skupper-router-proxies {{- if .Sizing.Adaptor.NotEmpty -}} {{- template "resources" .Sizing.Adaptor -}} {{- end }} @@ -181,6 +185,8 @@ spec: volumeMounts: - mountPath: /etc/skupper-router-certs name: skupper-router-certs + - mountPath: /etc/skupper-router-proxies + name: skupper-router-proxies {{- if .Sizing.Adaptor.NotEmpty -}} {{- template "resources" .Sizing.Adaptor -}} {{- end }} @@ -188,6 +194,8 @@ spec: volumes: - emptyDir: {} name: skupper-router-certs + - emptyDir: {} + name: skupper-router-proxies {{- if not .DisableSecCtx }} securityContext: runAsNonRoot: true diff --git a/internal/kube/site/site.go b/internal/kube/site/site.go index ee8abfa07..0cefe1c37 100644 --- a/internal/kube/site/site.go +++ b/internal/kube/site/site.go @@ -143,6 +143,7 @@ func (s *Site) routerMode() qdr.Mode { } const SSL_PROFILE_PATH = "/etc/skupper-router-certs" +const PROXY_PROFILE_PATH = "/etc/skupper-router-proxies" func (s *Site) Reconcile(siteDef *skupperv2alpha1.Site) error { err := s.reconcile(siteDef, false) @@ -1068,10 +1069,31 @@ func (s *Site) setBindingsConfiguredStatus(err error) { s.bindings.Map(cf, lf) } -func (s *Site) newLink(linkconfig *skupperv2alpha1.Link) *site.Link { - config := site.NewLink(linkconfig.ObjectMeta.Name, SSL_PROFILE_PATH) +func (s *Site) getProxyConfig(proxySetting string) (*site.ProxyConfig, error) { + if proxySetting != "" { + proxySecret, err := s.profiles.Cache.Get(s.namespace + "/" + proxySetting) + if proxySecret != nil && err == nil { + return &site.ProxyConfig{ + Host: string(proxySecret.Data["host"]), + Port: string(proxySecret.Data["port"]), + User: string(proxySecret.Data["username"]), + ProfilePath: PROXY_PROFILE_PATH}, nil + } else { + return nil, stderrors.New("Secret not found for proxy configuration") + } + } + return nil, nil +} + +func (s *Site) newLink(linkconfig *skupperv2alpha1.Link) (*site.Link, error) { + proxySetting := linkconfig.Spec.GetProxyConfiguration() + proxyConfig, err := s.getProxyConfig(proxySetting) + if err != nil { + return nil, err + } + config := site.NewLink(linkconfig.ObjectMeta.Name, SSL_PROFILE_PATH, proxyConfig) config.Update(linkconfig) - return config + return config, nil } func (s *Site) CheckLink(name string, linkconfig *skupperv2alpha1.Link) error { @@ -1085,19 +1107,32 @@ func (s *Site) CheckLink(name string, linkconfig *skupperv2alpha1.Link) error { func (s *Site) link(linkconfig *skupperv2alpha1.Link) error { var config *site.Link + prevProxyProfileName := "" + currentProxyProfileName := linkconfig.Spec.GetProxyConfiguration() if existing, ok := s.links[linkconfig.ObjectMeta.Name]; ok { + prevProxyProfileName = existing.Definition().Spec.GetProxyConfiguration() if existing.Update(linkconfig) || !existing.Definition().IsConfigured() { config = existing } } else { - config = s.newLink(linkconfig) - s.links[linkconfig.ObjectMeta.Name] = config + config, err := s.newLink(linkconfig) + if err == nil { + s.links[linkconfig.ObjectMeta.Name] = config + } else { + return s.updateLinkConfiguredCondition(linkconfig, err) + } } if s.initialised { if config != nil { s.logger.Info("Connecting site using token", slog.String("namespace", s.namespace), slog.String("token", linkconfig.ObjectMeta.Name)) + if currentProxyProfileName != "" && prevProxyProfileName != "" && currentProxyProfileName != prevProxyProfileName { + currentProxyConfig, err := s.getProxyConfig(currentProxyProfileName) + if err == nil { + config.UpdateProxyConfig(currentProxyConfig) + } + } err := s.updateRouterConfig(config) return s.updateLinkConfiguredCondition(linkconfig, err) } else { @@ -1207,11 +1242,11 @@ func (s *Site) updateLinkOperationalCondition(link *skupperv2alpha1.Link, operat return nil } -func (s *Site) CheckSslProfiles(config *qdr.RouterConfig) error { +func (s *Site) CheckSslAndProxyProfiles(config *qdr.RouterConfig) error { if !s.initialised || config == nil { return nil } - s.profiles.UseProfiles(config.SslProfiles) + s.profiles.UseProfiles(config.SslProfiles, config.ProxyProfiles) return nil } diff --git a/internal/kube/site/site_test.go b/internal/kube/site/site_test.go index e85ed8c46..987a7c066 100644 --- a/internal/kube/site/site_test.go +++ b/internal/kube/site/site_test.go @@ -871,94 +871,6 @@ func TestSite_CheckLink(t *testing.T) { }, }, }, - { - name: "link - not found", - args: args{ - name: "link1", - linkconfig: &skupperv2alpha1.Link{ - ObjectMeta: metav1.ObjectMeta{ - Name: "link1", - Namespace: "test", - UID: "8a96ffdf-403b-4e4a-83a8-97d3d459adb6", - }, - Spec: skupperv2alpha1.LinkSpec{ - Cost: 1, - Endpoints: []skupperv2alpha1.Endpoint{ - { - Name: string(qdr.RoleInterRouter), - Host: "10.10.10.1", - Port: "55671", - }, - }, - }, - }, - }, - want: "initialized", - wantErr: true, - wantLinks: 1, - skupperErrorMessage: "NotFound", - }, - { - name: "link - ok", - args: args{ - name: "link1", - linkconfig: &skupperv2alpha1.Link{ - ObjectMeta: metav1.ObjectMeta{ - Name: "link1", - Namespace: "test", - UID: "8a96ffdf-403b-4e4a-83a8-97d3d459adb6", - }, - Spec: skupperv2alpha1.LinkSpec{ - Cost: 2, - Endpoints: []skupperv2alpha1.Endpoint{ - { - Name: string(qdr.RoleInterRouter), - Host: "1.1.1.1", - Port: "55671", - }, - }, - }, - }, - }, - skupperObjects: []runtime.Object{ - &skupperv2alpha1.Link{ - ObjectMeta: metav1.ObjectMeta{ - Name: "link1", - Namespace: "test", - }, - }, - }, - want: "initialized", - wantErr: false, - wantLinks: 1, - }, - { - name: "link - error", - args: args{ - name: "link1", - linkconfig: &skupperv2alpha1.Link{ - ObjectMeta: metav1.ObjectMeta{ - Name: "link1", - Namespace: "test", - UID: "8a96ffdf-403b-4e4a-83a8-97d3d459adb6", - }, - Spec: skupperv2alpha1.LinkSpec{ - Cost: 2, - Endpoints: []skupperv2alpha1.Endpoint{ - { - Name: string(qdr.RoleInterRouter), - Host: "1.1.1.1", - Port: "55671", - }, - }, - }, - }, - }, - want: "initialized", - wantErr: true, - wantLinks: 1, - skupperErrorMessage: "NotFound", - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -984,8 +896,8 @@ func TestSite_CheckLink(t *testing.T) { if link.Spec.Cost != tt.args.linkconfig.Spec.Cost { t.Errorf("Site.CheckLink() link not configured correctly") } - for _, condition := range link.Status.Conditions { + t.Log("In check link", condition.Type, condition.Status) if condition.Type == "Configured" && condition.Status == "True" { linkConfigured = true } @@ -1191,7 +1103,8 @@ func Test_NetworkStatusUpdate(t *testing.T) { // add link if tt.linkconfig != nil { - link := s.newLink(tt.linkconfig) + link, err := s.newLink(tt.linkconfig) + assert.Assert(t, err) s.links[tt.linkconfig.ObjectMeta.Name] = link } diff --git a/internal/nonkube/bundle/site_state_renderer.go b/internal/nonkube/bundle/site_state_renderer.go index 674b56adc..f23eb839d 100644 --- a/internal/nonkube/bundle/site_state_renderer.go +++ b/internal/nonkube/bundle/site_state_renderer.go @@ -56,9 +56,10 @@ func (s *SiteStateRenderer) Render(loadedSiteState *api.SiteState, reload bool) s.siteState.CreateBridgeCertificates() // rendering non-kube configuration files and certificates s.configRenderer = &common.FileSystemConfigurationRenderer{ - SslProfileBasePath: "{{.SslProfileBasePath}}", - Platform: string(s.Platform), - Bundle: true, + SslProfileBasePath: "{{.SslProfileBasePath}}", + ProxyProfileBasePath: "{{.SslProfileBasePath}}", + Platform: string(s.Platform), + Bundle: true, } err = s.configRenderer.Render(s.siteState) if err != nil { diff --git a/internal/nonkube/common/fs_config_renderer.go b/internal/nonkube/common/fs_config_renderer.go index 2dc4efe96..373e011ca 100644 --- a/internal/nonkube/common/fs_config_renderer.go +++ b/internal/nonkube/common/fs_config_renderer.go @@ -50,16 +50,18 @@ const ( ) const ( - DefaultSslProfileBasePath = "${SSL_PROFILE_BASE_PATH}" + DefaultSslProfileBasePath = "${SSL_PROFILE_BASE_PATH}" + DefaultProxyProfileBasePath = "${SSL_PROFILE_BASE_PATH}" ) type FileSystemConfigurationRenderer struct { // SslProfileBasePath path where configuration will be read from in runtime - SslProfileBasePath string - RouterConfig qdr.RouterConfig - Platform string - Bundle bool - customOutputPath string + SslProfileBasePath string + ProxyProfileBasePath string + RouterConfig qdr.RouterConfig + Platform string + Bundle bool + customOutputPath string } func NewFileSystemConfigurationRenderer(outputPath string) *FileSystemConfigurationRenderer { diff --git a/internal/qdr/amqp_mgmt.go b/internal/qdr/amqp_mgmt.go index d2f0b750f..1ad19b779 100644 --- a/internal/qdr/amqp_mgmt.go +++ b/internal/qdr/amqp_mgmt.go @@ -885,6 +885,38 @@ func (a *Agent) GetSslProfiles() (map[string]SslProfile, error) { return profiles, nil } +func (a *Agent) GetProxyProfileByName(name string) (*ProxyProfile, error) { + + results, err := a.Query("io.skupper.router.proxyProfile", []string{}) + if err != nil { + return nil, err + } + for _, record := range results { + + result := asProxyProfile(record) + + if result.Name == name { + return &result, nil + } + } + + return nil, nil +} + +func (a *Agent) GetProxyProfiles() (map[string]ProxyProfile, error) { + results, err := a.Query("io.skupper.router.proxyProfile", []string{}) + if err != nil { + return nil, err + } + profiles := map[string]ProxyProfile{} + for _, record := range results { + profile := asProxyProfile(record) + profiles[profile.Name] = profile + } + + return profiles, nil +} + func (a *Agent) GetLocalTcpListeners(filter TcpEndpointFilter) ([]TcpEndpoint, error) { return a.getLocalTcpEndpoints("io.skupper.router.tcpListener", filter) } @@ -1178,6 +1210,7 @@ func asConnector(record Record) Connector { RouteContainer: record.AsBool("routeContainer"), VerifyHostname: record.AsBool("verifyHostname"), SslProfile: record.AsString("sslProfile"), + ProxyProfile: record.AsString("proxyProfile"), Cost: int32(record.AsInt("cost")), Role: Role(record.AsString("role")), } @@ -1221,6 +1254,16 @@ func asSslProfile(record Record) SslProfile { } } +func asProxyProfile(record Record) ProxyProfile { + return ProxyProfile{ + Name: record.AsString("name"), + Host: record.AsString("host"), + Port: record.AsString("port"), + Username: record.AsString("username"), + Password: record.AsString("password"), + } +} + func (a *Agent) UpdateConnectorConfig(changes *ConnectorDifference, checkCertFilesExist bool) error { for _, deleted := range changes.Deleted { if err := a.Delete("io.skupper.router.connector", deleted.Name); err != nil { @@ -1266,6 +1309,21 @@ func (a *Agent) UpdateConnectorConfig(changes *ConnectorDifference, checkCertFil } } + if len(added.ProxyProfile) > 0 { + proxyProfile, err := a.GetProxyProfileByName(added.ProxyProfile) + if err != nil { + return err + } + + if proxyProfile.Password != "" { + _, err := os.Stat(strings.TrimPrefix(proxyProfile.Password, "file:")) + if err != nil { + return err + } + } + + } + if err := a.Create("io.skupper.router.connector", added.Name, added); err != nil { return fmt.Errorf("Error adding connectors: %s", err) } @@ -1440,6 +1498,52 @@ func (a *Agent) ReloadSslProfile(name string) error { return nil } +func (a *Agent) CreateProxyProfile(profile ProxyProfile) error { + + result, err := a.GetProxyProfileByName(profile.Name) + if err != nil { + return err + } + + // Trying to create a proxy profile that already exists will generate an error in the router. + if result != nil { + return nil + } + + if err := a.Create("io.skupper.router.proxyProfile", profile.Name, profile); err != nil { + return fmt.Errorf("Error adding Proxy Profile: %s", err) + } + + return nil +} + +func (a *Agent) UpdateProxyProfile(profile ProxyProfile) error { + if err := a.Update("io.skupper.router.proxyProfile", profile.Name, profile); err != nil { + return fmt.Errorf("error updating Proxy Profile: %s", err) + } + + return nil +} + +func (a *Agent) ReloadProxyProfile(name string) error { + + profile, err := a.GetProxyProfileByName(name) + if err != nil { + return err + } + + // A profile is expected to be returned + if profile == nil { + return fmt.Errorf("No Proxy Profile with name %s found", name) + } + + if err := a.Update("io.skupper.router.proxyProfile", profile.Name, profile); err != nil { + return fmt.Errorf("Error updating Proxy Profile: %s", err) + } + + return nil +} + func ConnectedSitesInfo(selfId string, routers []Router) types.TransportConnectedSites { var connectedSites types.TransportConnectedSites var self *Router diff --git a/internal/qdr/qdr.go b/internal/qdr/qdr.go index 8d2d7849a..f6e733e2a 100644 --- a/internal/qdr/qdr.go +++ b/internal/qdr/qdr.go @@ -15,14 +15,15 @@ import ( ) type RouterConfig struct { - Metadata RouterMetadata - SslProfiles map[string]SslProfile - Listeners map[string]Listener - Connectors map[string]Connector - Addresses map[string]Address - LogConfig map[string]LogConfig - SiteConfig *SiteConfig - Bridges BridgeConfig + Metadata RouterMetadata + SslProfiles map[string]SslProfile + ProxyProfiles map[string]ProxyProfile + Listeners map[string]Listener + Connectors map[string]Connector + Addresses map[string]Address + LogConfig map[string]LogConfig + SiteConfig *SiteConfig + Bridges BridgeConfig } type RouterConfigHandler interface { @@ -52,11 +53,12 @@ func InitialConfig(id string, siteId string, version string, edge bool, helloAge HelloMaxAgeSeconds: strconv.Itoa(helloAge), Metadata: getSiteMetadataString(siteId, version), }, - Addresses: map[string]Address{}, - SslProfiles: map[string]SslProfile{}, - Listeners: map[string]Listener{}, - Connectors: map[string]Connector{}, - LogConfig: map[string]LogConfig{}, + Addresses: map[string]Address{}, + SslProfiles: map[string]SslProfile{}, + ProxyProfiles: map[string]ProxyProfile{}, + Listeners: map[string]Listener{}, + Connectors: map[string]Connector{}, + LogConfig: map[string]LogConfig{}, Bridges: BridgeConfig{ TcpListeners: map[string]TcpEndpoint{}, TcpConnectors: map[string]TcpEndpoint{}, @@ -213,6 +215,62 @@ func (r *RouterConfig) UnreferencedSslProfiles() map[string]SslProfile { return results } +const PROXY_PATH_PREFIX = "file:" +const PROXY_PASSWORD_FILE = "password.txt" + +func ConfigureProxyProfile(name string, host string, port string, username string, path string) ProxyProfile { + profile := ProxyProfile{ + Name: name, + Host: host, + Port: port, + } + if username != "" && path != "" { + profile.Username = username + profile.Password = path_.Join(PROXY_PATH_PREFIX, path, name, PROXY_PASSWORD_FILE) + } + return profile +} + +func (r *RouterConfig) AddProxyProfile(p ProxyProfile) bool { + if original, ok := r.ProxyProfiles[p.Name]; ok && original == p { + return false + } + r.ProxyProfiles[p.Name] = p + return true +} + +func (r *RouterConfig) RemoveProxyProfile(name string) bool { + _, ok := r.ProxyProfiles[name] + if ok { + delete(r.ProxyProfiles, name) + return true + } else { + return false + } +} + +func (r *RouterConfig) RemoveUnreferencedProxyProfiles() bool { + unreferenced := r.UnreferencedProxyProfiles() + changed := false + for _, profile := range unreferenced { + if r.RemoveProxyProfile(profile.Name) { + changed = true + } + } + return changed +} + +func (r *RouterConfig) UnreferencedProxyProfiles() map[string]ProxyProfile { + results := map[string]ProxyProfile{} + for _, profile := range r.ProxyProfiles { + results[profile.Name] = profile + } + for _, o := range r.Connectors { + delete(results, o.ProxyProfile) + } + return results +} + func (r *RouterConfig) AddAddress(a Address) { r.Addresses[a.Prefix] = a } @@ -528,6 +586,34 @@ func (l *Listener) SetMaxSessionFrames(value int) { l.MaxSessionFrames = value } +type ProxyProfile struct { + Name string `json:"name,omitempty"` + Host string `json:"host,omitempty"` + Port string `json:"port,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +func (p ProxyProfile) toRecord() Record { + result := make(map[string]any) + if p.Name != "" { + result["name"] = p.Name + } + if p.Host != "" { + result["host"] = p.Host + } + if p.Port != "" { + result["port"] = p.Port + } + if p.Username != "" { + result["username"] = p.Username + } + if p.Password != "" { + result["password"] = p.Password + } + return result +} + type Connector struct { Name string `json:"name,omitempty"` Role Role `json:"role,omitempty"` @@ -537,6 +623,7 @@ type Connector struct { Cost int32 `json:"cost,omitempty"` VerifyHostname bool `json:"verifyHostname,omitempty"` SslProfile string `json:"sslProfile,omitempty"` + ProxyProfile string `json:"proxyProfile,omitempty"` LinkCapacity int32 `json:"linkCapacity,omitempty"` MaxFrameSize int `json:"maxFrameSize,omitempty"` MaxSessionFrames int `json:"maxSessionFrames,omitempty"` @@ -554,6 +641,9 @@ func (connector Connector) toRecord() Record { if len(connector.SslProfile) > 0 { record["sslProfile"] = connector.SslProfile } + if len(connector.ProxyProfile) > 0 { + record["proxyProfile"] = connector.ProxyProfile + } if connector.MaxFrameSize > 0 { record["maxFrameSize"] = connector.MaxFrameSize } @@ -693,12 +783,13 @@ func RouterConfigEquals(actual, desired string) bool { func UnmarshalRouterConfig(config string) (RouterConfig, error) { result := RouterConfig{ - Metadata: RouterMetadata{}, - Addresses: map[string]Address{}, - SslProfiles: map[string]SslProfile{}, - Listeners: map[string]Listener{}, - Connectors: map[string]Connector{}, - LogConfig: map[string]LogConfig{}, + Metadata: RouterMetadata{}, + Addresses: map[string]Address{}, + SslProfiles: map[string]SslProfile{}, + ProxyProfiles: map[string]ProxyProfile{}, + Listeners: map[string]Listener{}, + Connectors: map[string]Connector{}, + LogConfig: map[string]LogConfig{}, Bridges: BridgeConfig{ TcpListeners: map[string]TcpEndpoint{}, TcpConnectors: map[string]TcpEndpoint{}, @@ -759,6 +850,13 @@ func UnmarshalRouterConfig(config string) (RouterConfig, error) { return result, fmt.Errorf("Invalid %s element got %#v", entityType, element[1]) } result.SslProfiles[sslProfile.Name] = sslProfile + case "proxyProfile": + proxyProfile := ProxyProfile{} + err = convert(element[1], &proxyProfile) + if err != nil { + return result, fmt.Errorf("Invalid %s element got %#v", entityType, element[1]) + } + result.ProxyProfiles[proxyProfile.Name] = proxyProfile case "log": logConfig := LogConfig{} err = convert(element[1], &logConfig) @@ -814,6 +912,13 @@ func MarshalRouterConfig(config RouterConfig) (string, error) { } elements = append(elements, tuple) } + for _, e := range config.ProxyProfiles { + tuple := []interface{}{ + "proxyProfile", + e, + } + elements = append(elements, tuple) + } for _, e := range config.Connectors { tuple := []interface{}{ "connector", @@ -984,9 +1089,10 @@ func GetBridgeConfigFromConfigMap(configmap *corev1.ConfigMap) (*BridgeConfig, e } type ConnectorDifference struct { - Deleted []Connector - Added []Connector - AddedSslProfiles map[string]SslProfile + Deleted []Connector + Added []Connector + AddedSslProfiles map[string]SslProfile + AddedProxyProfiles map[string]ProxyProfile } type TcpEndpointDifference struct { @@ -1206,11 +1312,13 @@ func (a *BridgeConfigDifference) Print() { func ConnectorsDifference(actual map[string]Connector, desired *RouterConfig, ignorePrefix *string) *ConnectorDifference { result := ConnectorDifference{} result.AddedSslProfiles = make(map[string]SslProfile) + result.AddedProxyProfiles = make(map[string]ProxyProfile) for key, v1 := range desired.Connectors { actualValue, ok := actual[key] if !ok { result.Added = append(result.Added, v1) result.AddedSslProfiles[v1.SslProfile] = desired.SslProfiles[v1.SslProfile] + result.AddedProxyProfiles[v1.ProxyProfile] = desired.ProxyProfiles[v1.ProxyProfile] } //in case the link connector exists but has changed some of its values, it needs to be recreated again diff --git a/internal/qdr/qdr_test.go b/internal/qdr/qdr_test.go index 268f26f0f..8d063fec2 100644 --- a/internal/qdr/qdr_test.go +++ b/internal/qdr/qdr_test.go @@ -153,6 +153,23 @@ func TestAddSslProfile(t *testing.T) { assert.Equal(t, config.SslProfiles["third"].PrivateKeyFile, "") } +func TestAddProxyProfile(t *testing.T) { + config := InitialConfig("foo", "bar", "undefined", true, 3) + config.AddProxyProfile(ProxyProfile{ + Name: "myprofile", + Host: "192.168.0.1", + }) + assert.Equal(t, config.ProxyProfiles["myprofile"].Host, "192.168.0.1") + + config.AddProxyProfile(ConfigureProxyProfile("another", "192.168.1.1", "8080", "user1", "password1")) + assert.Equal(t, config.ProxyProfiles["another"].Host, "192.168.1.1") + assert.Equal(t, config.ProxyProfiles["another"].Port, "8080") + + config.AddProxyProfile(ConfigureProxyProfile("third", "192.168.2.1", "9090", "user2", "password2")) + assert.Equal(t, config.ProxyProfiles["third"].Host, "192.168.2.1") + assert.Equal(t, config.ProxyProfiles["third"].Port, "9090") +} + func TestAddAddress(t *testing.T) { config := InitialConfig("foo", "bar", "undefined", true, 3) config.AddAddress(Address{ @@ -189,18 +206,36 @@ func TestMarshalUnmarshalRouterConfig(t *testing.T) { PrivateKeyFile: "/somewhere/else/myKey.pem", }, }, + ProxyProfiles: map[string]ProxyProfile{ + "one-proxy": ProxyProfile{ + Name: "one-proxy", + Host: "192.168.0.1", + Port: "8080", + Username: "barney", + Password: "rubble", + }, + "two-proxy": ProxyProfile{ + Name: "two-proxy", + Host: "192.168.1.1", + Port: "9090", + Username: "fred", + Password: "flinstone", + }, + }, Connectors: map[string]Connector{ "c1": Connector{ - Name: "c1", - Host: "somewhere.com", - Port: "1234", - SslProfile: "one", + Name: "c1", + Host: "somewhere.com", + Port: "1234", + SslProfile: "one", + ProxyProfile: "one-proxy", }, "c2": Connector{ - Name: "c2", - Host: "elsewhere.com", - Port: "5678", - SslProfile: "two", + Name: "c2", + Host: "elsewhere.com", + Port: "5678", + SslProfile: "two", + ProxyProfile: "two-proxy", }, }, Listeners: map[string]Listener{ @@ -289,6 +324,9 @@ func TestMarshalUnmarshalRouterConfig(t *testing.T) { if !reflect.DeepEqual(input.SslProfiles, output.SslProfiles) { t.Errorf("Incorrect sslprofiles. Expected %#v got %#v", input.SslProfiles, output.SslProfiles) } + if !reflect.DeepEqual(input.ProxyProfiles, output.ProxyProfiles) { + t.Errorf("Incorrect proxyprofiles. Expected %#v got %#v", input.ProxyProfiles, output.ProxyProfiles) + } if !reflect.DeepEqual(input.Connectors, output.Connectors) { t.Errorf("Incorrect connectors. Expected %#v got %#v", input.Connectors, output.Connectors) } @@ -348,6 +386,13 @@ func TestUnmarshalErrorInvalidSslProfileValue(t *testing.T) { } } +func TestUnmarshalErrorInvalidProxyProfileValue(t *testing.T) { + _, err := UnmarshalRouterConfig(`[["proxyProfile", ["wrong"]]]`) + if err == nil { + t.Errorf("Expected error for invalid proxyprofile value") + } +} + func TestUnmarshalErrorInvalidRouterValue(t *testing.T) { _, err := UnmarshalRouterConfig(`[["router", ["wrong"]]]`) if err == nil { @@ -649,6 +694,9 @@ func TestRecordTypes_GH2081(t *testing.T) { SslProfile{ Name: "myprofile", }, + ProxyProfile{ + Name: "myproxyprofile", + }, } for _, rt := range testCases { t.Run("", func(t *testing.T) { diff --git a/internal/site/link.go b/internal/site/link.go index f0dcce01e..05ae02f8c 100644 --- a/internal/site/link.go +++ b/internal/site/link.go @@ -8,17 +8,34 @@ import ( skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1" ) +type ProxyConfig struct { + Host string + Port string + User string + ProfilePath string +} + type Link struct { - name string - profilePath string - definition *skupperv2alpha1.Link + name string + sslProfilePath string + proxyConfig *ProxyConfig + definition *skupperv2alpha1.Link } -func NewLink(name string, profilePath string) *Link { +func NewLink(name string, sslProfilePath string, proxyConfig *ProxyConfig) *Link { return &Link{ - name: name, - profilePath: profilePath, + name: name, + sslProfilePath: sslProfilePath, + proxyConfig: proxyConfig, + } +} + +func (l *Link) UpdateProxyConfig(proxyConfig *ProxyConfig) bool { + if l.definition == nil { + return false } + l.proxyConfig = proxyConfig + return true } func (l *Link) Apply(current *qdr.RouterConfig) bool { @@ -33,21 +50,32 @@ func (l *Link) Apply(current *qdr.RouterConfig) bool { if !ok { return false } - profileName := sslProfileName(l.definition) + sslProfileName := sslProfileName(l.definition) + proxyProfileName := proxyProfileName(l.definition) + prevProxyProfileName := current.Connectors[l.name].ProxyProfile cost := int32(l.definition.Spec.Cost) if cost < 1 { cost = 1 } connector := qdr.Connector{ - Name: l.name, - Cost: cost, - SslProfile: profileName, - Role: role, - Host: endpoint.Host, - Port: endpoint.Port, + Name: l.name, + Cost: cost, + SslProfile: sslProfileName, + ProxyProfile: proxyProfileName, + Role: role, + Host: endpoint.Host, + Port: endpoint.Port, } current.AddConnector(connector) - current.AddSslProfile(qdr.ConfigureSslProfile(profileName, l.profilePath, true)) + current.AddSslProfile(qdr.ConfigureSslProfile(sslProfileName, l.sslProfilePath, true)) + if proxyProfileName != "" { + current.AddProxyProfile(qdr.ConfigureProxyProfile(proxyProfileName, l.proxyConfig.Host, l.proxyConfig.Port, l.proxyConfig.User, l.proxyConfig.ProfilePath)) + if prevProxyProfileName != "" && prevProxyProfileName != proxyProfileName { + current.RemoveProxyProfile(prevProxyProfileName) + } + } else if prevProxyProfileName != "" { + current.RemoveProxyProfile(prevProxyProfileName) + } return true //TODO: optimise by indicating if no change was actually needed } @@ -55,6 +83,10 @@ func sslProfileName(link *skupperv2alpha1.Link) string { return link.Spec.TlsCredentials + "-profile" } +func proxyProfileName(link *skupperv2alpha1.Link) string { + return link.Spec.GetProxyConfiguration() +} + type LinkMap map[string]*Link func (m LinkMap) Apply(current *qdr.RouterConfig) bool { @@ -66,6 +98,7 @@ func (m LinkMap) Apply(current *qdr.RouterConfig) bool { if _, ok := m[connector.Name]; !ok { current.RemoveConnector(connector.Name) current.RemoveSslProfile(connector.SslProfile) + current.RemoveProxyProfile(connector.ProxyProfile) } } } @@ -92,6 +125,10 @@ func (o *RemoveConnector) Apply(current *qdr.RouterConfig) bool { if _, ok := unreferenced[connector.SslProfile]; ok { current.RemoveSslProfile(connector.SslProfile) } + unreferencedProxyProfiles := current.UnreferencedProxyProfiles() + if _, ok := unreferencedProxyProfiles[connector.ProxyProfile]; ok { + current.RemoveProxyProfile(connector.ProxyProfile) + } return true } return false diff --git a/internal/site/link_test.go b/internal/site/link_test.go index 959a77015..e04afab6b 100644 --- a/internal/site/link_test.go +++ b/internal/site/link_test.go @@ -18,9 +18,10 @@ func TestLink_Apply(t *testing.T) { helloAge := 10 type fields struct { - name string - profilePath string - definition *skupperv2alpha1.Link + name string + sslProfilePath string + proxyConfig ProxyConfig + definition *skupperv2alpha1.Link } type args struct { current qdr.RouterConfig @@ -34,9 +35,10 @@ func TestLink_Apply(t *testing.T) { { name: "no definition", fields: fields{ - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", - definition: nil, + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + proxyConfig: ProxyConfig{}, + definition: nil, }, args: args{ current: qdr.InitialConfig(id, siteId, version, notEdge, helloAge), @@ -46,8 +48,9 @@ func TestLink_Apply(t *testing.T) { { name: "inter router definition but no endpoint", fields: fields{ - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + proxyConfig: ProxyConfig{}, definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "old-site", @@ -64,8 +67,9 @@ func TestLink_Apply(t *testing.T) { { name: "inter router definition with endpoint", fields: fields{ - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + proxyConfig: ProxyConfig{}, definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "old-site", @@ -90,8 +94,9 @@ func TestLink_Apply(t *testing.T) { { name: "edge router definition with endpoint", fields: fields{ - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + proxyConfig: ProxyConfig{}, definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "old-site", @@ -116,7 +121,7 @@ func TestLink_Apply(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := NewLink(tt.fields.name, tt.fields.profilePath) + l := NewLink(tt.fields.name, tt.fields.sslProfilePath, &tt.fields.proxyConfig) l.definition = tt.fields.definition if got := l.Apply(&tt.args.current); got != tt.want { t.Errorf("Link.Apply() = %v, want %v", got, tt.want) @@ -149,9 +154,9 @@ func TestLinkMap_Apply(t *testing.T) { name: "no definition", links: []Link{ { - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", - definition: nil, + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + definition: nil, }, }, connectors: []qdr.Connector{}, @@ -162,8 +167,8 @@ func TestLinkMap_Apply(t *testing.T) { name: "inter router definition", links: []Link{ { - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "site-1", @@ -189,8 +194,8 @@ func TestLinkMap_Apply(t *testing.T) { name: "edge definition", links: []Link{ { - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "site-1", @@ -217,8 +222,8 @@ func TestLinkMap_Apply(t *testing.T) { name: "two links", links: []Link{ { - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "site-1", @@ -236,8 +241,8 @@ func TestLinkMap_Apply(t *testing.T) { }, }, { - name: "link2", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link2", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "site-2", @@ -263,8 +268,8 @@ func TestLinkMap_Apply(t *testing.T) { name: "remove a connection", links: []Link{ { - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "site-1", @@ -316,9 +321,9 @@ func TestLinkMap_Apply(t *testing.T) { func TestLink_Update(t *testing.T) { type fields struct { - name string - profilePath string - definition *skupperv2alpha1.Link + name string + sslProfilePath string + definition *skupperv2alpha1.Link } type args struct { definition *skupperv2alpha1.Link @@ -332,8 +337,8 @@ func TestLink_Update(t *testing.T) { { name: "links equal", fields: fields{ - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "site-1", @@ -372,8 +377,8 @@ func TestLink_Update(t *testing.T) { { name: "links not equal", fields: fields{ - name: "link1", - profilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", + name: "link1", + sslProfilePath: "/etc/skupper-router-certs/skupper-internal/ca.crt", definition: &skupperv2alpha1.Link{ ObjectMeta: v1.ObjectMeta{ Name: "site-1", @@ -413,9 +418,9 @@ func TestLink_Update(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { link := &Link{ - name: tt.fields.name, - profilePath: tt.fields.profilePath, - definition: tt.fields.definition, + name: tt.fields.name, + sslProfilePath: tt.fields.sslProfilePath, + definition: tt.fields.definition, } if got := link.Update(tt.args.definition); got != tt.want { t.Errorf("Link.Update() = %v, want %v", got, tt.want) diff --git a/pkg/apis/skupper/v2alpha1/types.go b/pkg/apis/skupper/v2alpha1/types.go index 50db39420..7c79255e9 100644 --- a/pkg/apis/skupper/v2alpha1/types.go +++ b/pkg/apis/skupper/v2alpha1/types.go @@ -583,6 +583,13 @@ type LinkSpec struct { Settings map[string]string `json:"settings,omitempty"` } +func (s *LinkSpec) GetProxyConfiguration() string { + if value, ok := s.Settings["proxy-configuration"]; ok { + return value + } + return "" +} + func (s *LinkSpec) GetEndpointForRole(name string) (Endpoint, bool) { for _, endpoint := range s.Endpoints { if endpoint.Name == name { diff --git a/pkg/nonkube/api/environment.go b/pkg/nonkube/api/environment.go index f055d818e..7f23aed2a 100644 --- a/pkg/nonkube/api/environment.go +++ b/pkg/nonkube/api/environment.go @@ -14,9 +14,11 @@ type InternalPathProvider func(namespace string, internalPath InternalPath) stri const ( InputIssuersPath InternalPath = "input/issuers" InputCertificatesPath InternalPath = "input/certs" + InputProxyProfilePath InternalPath = "input/proxies" InputSiteStatePath InternalPath = "input/resources" RouterConfigPath InternalPath = "runtime/router" CertificatesPath InternalPath = "runtime/certs" + ProxyProfilesPath InternalPath = "runtime/proxies" IssuersPath InternalPath = "runtime/issuers" RuntimePath InternalPath = "runtime" RuntimeSiteStatePath InternalPath = "runtime/resources" diff --git a/pkg/nonkube/api/site_state.go b/pkg/nonkube/api/site_state.go index 9bd929690..88e2748c6 100644 --- a/pkg/nonkube/api/site_state.go +++ b/pkg/nonkube/api/site_state.go @@ -267,7 +267,8 @@ func (s *SiteState) linkAccessMap() site.RouterAccessMap { func (s *SiteState) linkMap(sslProfileBasePath string) site.LinkMap { linkMap := site.LinkMap{} for name, link := range s.Links { - siteLink := site.NewLink(name, path.Join(sslProfileBasePath, string(CertificatesPath))) + // TODO: proxy profile config ? + siteLink := site.NewLink(name, path.Join(sslProfileBasePath, string(CertificatesPath)), &site.ProxyConfig{}) link.SetConfigured(nil) siteLink.Update(link) linkMap[name] = siteLink