diff --git a/cmd/all_in_one.go b/cmd/all_in_one.go index 2dfc873d..7d591ae5 100644 --- a/cmd/all_in_one.go +++ b/cmd/all_in_one.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "net" "net/url" "os" "strings" @@ -218,7 +219,7 @@ func (s *allCmdParam) run(ctx context.Context) error { eg, ctx := errgroup.WithContext(ctx) cfgCtl := util.NewRestartOnChange[*config.Config]() - runner, err := pomerium_ctrl.NewPomeriumRunner(s.cfg, cfgCtl.OnConfigUpdated) + runner, err := pomerium_ctrl.NewPomeriumRunner(s.cfg, cfgCtl.OnConfigUpdated, s.syncAPIURL) if err != nil { return fmt.Errorf("preparing to run pomerium: %w", err) } @@ -339,7 +340,13 @@ func (s *allCmdParam) buildController(ctx context.Context, cfg *config.Config) ( client := databroker.NewDataBrokerServiceClient(conn) var reconciler pomerium.Reconciler if s.syncAPIURL != "" { - reconciler = pomerium.NewAPIReconciler(s.syncAPIURL, s.syncAPIToken, s.cfg.Options) + _, port, err := net.SplitHostPort(s.cfg.Options.Addr) + if err != nil { + return nil, fmt.Errorf("couldn't get server address port: %w", err) + } + dialAddressOverride := net.JoinHostPort("localhost", port) + reconciler = pomerium.NewAPIReconciler( + s.syncAPIURL, s.syncAPIToken, s.cfg.Options, dialAddressOverride) } else { reconciler = pomerium.NewDataBrokerReconciler(client, s.dumpConfigDiff) } @@ -365,9 +372,14 @@ func (s *allCmdParam) buildController(ctx context.Context, cfg *config.Config) ( return c, nil } +type bootstrapReconciler interface { + pomerium.ConfigReconciler + pomerium.IngressReconciler +} + // runBootstrapConfigController runs a controller that only listens to changes in SettingsCRD // related to pomerium bootstrap parameters -func (s *allCmdParam) runBootstrapConfigController(ctx context.Context, reconciler pomerium.ConfigReconciler) error { +func (s *allCmdParam) runBootstrapConfigController(ctx context.Context, reconciler bootstrapReconciler) error { scheme, err := getScheme() if err != nil { return err @@ -389,10 +401,16 @@ func (s *allCmdParam) runBootstrapConfigController(ctx context.Context, reconcil if host, err := os.Hostname(); err == nil { name = fmt.Sprintf("%s pod/%s", name, host) } - // TODO: do we need to disable the bootstrap config controller when syncing via the API? if err := settings.NewSettingsController(mgr, reconciler, s.settings, name, false, health_ctrl.SettingsBootstrapReconciler); err != nil { return fmt.Errorf("settings controller: %w", err) } + if s.syncAPIURL != "" { + // When using the sync API in all-in-one mode, also register a bootstrap + // ingress controller, in case we need a route to the sync API itself. + if err := ingress.NewIngressController(mgr, reconciler); err != nil { + return fmt.Errorf("ingress controller: %w", err) + } + } return mgr.Start(ctx) } diff --git a/cmd/controller.go b/cmd/controller.go index 892e5508..f6ffcbdd 100644 --- a/cmd/controller.go +++ b/cmd/controller.go @@ -169,7 +169,7 @@ func (s *controllerCmd) buildController(ctx context.Context) (*controllers.Contr if s.SyncAPIURL != "" { c.Reconciler = pomerium.NewAPIReconciler( - s.SyncAPIURL, s.SyncAPIToken, pomerium_config.NewDefaultOptions()) + s.SyncAPIURL, s.SyncAPIToken, pomerium_config.NewDefaultOptions(), "") c.MgrOpts.LeaderElection = true c.MgrOpts.LeaderElectionID = s.leaderElectionID c.MgrOpts.LeaderElectionNamespace = s.leaderElectionNamespace diff --git a/pomerium/ctrl/bootstrap.go b/pomerium/ctrl/bootstrap.go index 9f75535b..63c9cb78 100644 --- a/pomerium/ctrl/bootstrap.go +++ b/pomerium/ctrl/bootstrap.go @@ -15,9 +15,12 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/pomerium/pomerium/config" + configpb "github.com/pomerium/pomerium/pkg/grpc/config" + "github.com/pomerium/ingress-controller/internal/filemgr" "github.com/pomerium/ingress-controller/model" - "github.com/pomerium/pomerium/config" + "github.com/pomerium/ingress-controller/pomerium" ) // Apply prepares a minimal bootstrap configuration for Pomerium @@ -211,3 +214,102 @@ func applySecrets(_ context.Context, dst *config.Options, src *model.Config) err return nil } + +// ApplyAdditional propagates additional core bootstrap settings, needed when +// running all-in-one mode together with Enterprise API sync. +func ApplyAdditional(ctx context.Context, dst *config.Options, src *model.Config) error { + var pbConfig configpb.Config + if err := pomerium.ApplyConfig(ctx, &pbConfig, src); err != nil { + return err + } + dst.ApplySettings(ctx, nil, pbConfig.Settings) + return nil +} + +// bootstrapIngressManager tracks Ingress resources matching a particular host. +type bootstrapIngressManager struct { + host string + routes map[types.NamespacedName][]config.Policy + updateFn func(context.Context) +} + +func newBootstrapIngressManager(host string, updateFn func(context.Context)) *bootstrapIngressManager { + return &bootstrapIngressManager{ + host: host, + routes: make(map[types.NamespacedName][]config.Policy), + updateFn: updateFn, + } +} + +func (m *bootstrapIngressManager) addRoutesToConfig(cfg *config.Config) { + if m == nil { + return + } + for _, routes := range m.routes { + cfg.Options.Routes = append(cfg.Options.Routes, routes...) + } +} + +func (m *bootstrapIngressManager) matchesHost(ic *model.IngressConfig) bool { + for _, rule := range ic.Spec.Rules { + if rule.Host == m.host { + return true + } + } + return false +} + +func (m *bootstrapIngressManager) deleteRoutes(ctx context.Context, name types.NamespacedName) (changes bool) { + changes = len(m.routes[name]) > 0 + delete(m.routes, name) + if changes { + m.updateFn(ctx) + } + return changes +} + +func (m *bootstrapIngressManager) addRoutes(ctx context.Context, ics ...*model.IngressConfig) (changes bool, err error) { + for _, ic := range ics { + if !m.matchesHost(ic) { + continue + } + routes, err := pomerium.IngressToRoutes(ctx, ic) + if err != nil { + return changes, err + } + m.routes[ic.GetIngressNamespacedName()] = routes + changes = true + } + if changes { + m.updateFn(ctx) + } + return changes, nil +} + +func (m *bootstrapIngressManager) Upsert(ctx context.Context, ic *model.IngressConfig) (changes bool, err error) { + // If this Ingress no longer matches the specific host, remove any + // previous matching routes. + if !m.matchesHost(ic) { + changes = m.deleteRoutes(ctx, ic.GetIngressNamespacedName()) + return changes, nil + } + return m.addRoutes(ctx, ic) +} + +// Set adds bootstrap routes for any ingresses matching the sync API URL. +func (m *bootstrapIngressManager) Set(ctx context.Context, ics []*model.IngressConfig) (changes bool, err error) { + if len(m.routes) > 0 { + changes = true + } + clear(m.routes) + + added, err := m.addRoutes(ctx, ics...) + changes = changes || added + return changes, err +} + +// Delete removes any bootstrap routes corresponding to the given ingress name. +func (m *bootstrapIngressManager) Delete(ctx context.Context, namespacedName types.NamespacedName) (changes bool, err error) { + changes = m.deleteRoutes(ctx, namespacedName) + return changes, nil +} diff --git a/pomerium/ctrl/bootstrap_test.go b/pomerium/ctrl/bootstrap_test.go index ff3c84bf..af239a41 100644 --- a/pomerium/ctrl/bootstrap_test.go +++ b/pomerium/ctrl/bootstrap_test.go @@ -7,7 +7,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - v1 "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "github.com/pomerium/pomerium/config" @@ -39,7 +41,7 @@ func TestSecretsDecodeRules(t *testing.T) { var opts config.Options assert.NoError(t, applySecrets(context.Background(), &opts, &model.Config{ - Secrets: &v1.Secret{ + Secrets: &corev1.Secret{ Data: map[string][]byte{ "shared_secret": mustB64Decode(t, "9OkZR6hwfmVD3a7Sfmgq58lUbFJGGz4hl/R9xbHFCAg="), "cookie_secret": mustB64Decode(t, "WwMtDXWaRDMBQCylle8OJ+w4kLIDIGd8W3cB4/zFFtg="), @@ -50,7 +52,7 @@ func TestSecretsDecodeRules(t *testing.T) { assert.Error(t, applySecrets(context.Background(), &opts, &model.Config{})) assert.Error(t, applySecrets(context.Background(), &opts, &model.Config{ - Secrets: &v1.Secret{ + Secrets: &corev1.Secret{ Data: map[string][]byte{ "cookie_secret": mustB64Decode(t, "WwMtDXWaRDMBQCylle8OJ+w4kLIDIGd8W3cB4/zFFtg="), "signing_key": mustB64Decode(t, "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUhQbkN5MXk0TEZZVkhQb3RzM05rUSttTXJLcDgvVmVWRkRwaUk2TVNxMlVvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFT1h0VXAxOWFwRnNvVWJoYkI2cExMR1o1WFBXRlE5YWtmeW5ISy9RZ3paNC9MRjZhWEY2egpvS3lHMnNtL2wyajFiQ1JxUGJNd3dEVW9iWFNIODVIeDdRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="), @@ -58,7 +60,7 @@ func TestSecretsDecodeRules(t *testing.T) { }, })) assert.Error(t, applySecrets(context.Background(), &opts, &model.Config{ - Secrets: &v1.Secret{ + Secrets: &corev1.Secret{ Data: map[string][]byte{ "shared_secret": {1, 2, 3}, "cookie_secret": mustB64Decode(t, "WwMtDXWaRDMBQCylle8OJ+w4kLIDIGd8W3cB4/zFFtg="), @@ -67,3 +69,238 @@ func TestSecretsDecodeRules(t *testing.T) { }, })) } + +func TestBootstrapIngressManager(t *testing.T) { + // Helper to create a minimal IngressConfig for testing. + makeIngressConfig := func(name, namespace, host string) *model.IngressConfig { + return &model.IngressConfig{ + Ingress: &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: networkingv1.IngressSpec{ + Rules: []networkingv1.IngressRule{{ + Host: host, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{{ + Path: "/", + PathType: new(networkingv1.PathTypePrefix), + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "backend-svc", + Port: networkingv1.ServiceBackendPort{Number: 80}, + }, + }, + }}, + }, + }, + }}, + }, + }, + Services: map[types.NamespacedName]*corev1.Service{ + {Name: "backend-svc", Namespace: namespace}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "backend-svc", + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{ + Port: 80, + }}, + }, + }, + }, + } + } + + t.Run("Upsert", func(t *testing.T) { + t.Run("adds routes for matching ingress", func(t *testing.T) { + var updateCalled int + m := newBootstrapIngressManager("api.example.com", func(context.Context) { + updateCalled++ + }) + + ic := makeIngressConfig("my-ingress", "default", "api.example.com") + changes, err := m.Upsert(t.Context(), ic) + require.NoError(t, err) + assert.True(t, changes) + assert.Equal(t, 1, updateCalled) + assert.Len(t, m.routes, 1) + }) + + t.Run("no changes for non-matching ingress", func(t *testing.T) { + var updateCalled int + m := newBootstrapIngressManager("api.example.com", func(context.Context) { + updateCalled++ + }) + + ic := makeIngressConfig("my-ingress", "default", "other.example.com") + changes, err := m.Upsert(t.Context(), ic) + require.NoError(t, err) + assert.False(t, changes) + assert.Equal(t, 0, updateCalled) + assert.Len(t, m.routes, 0) + }) + + t.Run("removes routes when ingress no longer matches", func(t *testing.T) { + var updateCalled int + m := newBootstrapIngressManager("api.example.com", func(context.Context) { + updateCalled++ + }) + + // First add matching ingress + ic := makeIngressConfig("my-ingress", "default", "api.example.com") + _, err := m.Upsert(t.Context(), ic) + require.NoError(t, err) + assert.Len(t, m.routes, 1) + + // Now update to non-matching + ic.Spec.Rules[0].Host = "other.example.com" + changes, err := m.Upsert(t.Context(), ic) + require.NoError(t, err) + assert.True(t, changes) + assert.Len(t, m.routes, 0) + assert.Equal(t, 2, updateCalled) + }) + + t.Run("updates routes when ingress changes", func(t *testing.T) { + var updateCalled int + m := newBootstrapIngressManager("api.example.com", func(context.Context) { + updateCalled++ + }) + + ic := makeIngressConfig("my-ingress", "default", "api.example.com") + _, err := m.Upsert(t.Context(), ic) + require.NoError(t, err) + initialRoutes := m.routes[types.NamespacedName{Name: "my-ingress", Namespace: "default"}] + + // Upsert same ingress again (simulating an update) + changes, err := m.Upsert(t.Context(), ic) + require.NoError(t, err) + assert.True(t, changes) + assert.Equal(t, 2, updateCalled) + assert.Len(t, initialRoutes, len(m.routes[types.NamespacedName{Name: "my-ingress", Namespace: "default"}])) + }) + }) + + t.Run("Set", func(t *testing.T) { + t.Run("replaces all routes", func(t *testing.T) { + var updateCalled int + m := newBootstrapIngressManager("api.example.com", func(context.Context) { + updateCalled++ + }) + + // Pre-populate with an ingress + ic1 := makeIngressConfig("ingress-1", "default", "api.example.com") + _, err := m.Upsert(t.Context(), ic1) + require.NoError(t, err) + assert.Equal(t, 1, updateCalled) + + // Set with a different list + ic2 := makeIngressConfig("ingress-2", "other-ns", "api.example.com") + changes, err := m.Set(t.Context(), []*model.IngressConfig{ic2}) + require.NoError(t, err) + assert.True(t, changes) + assert.Equal(t, 2, updateCalled) + assert.Len(t, m.routes, 1) + + _, hasOld := m.routes[types.NamespacedName{Name: "ingress-1", Namespace: "default"}] + assert.False(t, hasOld, "old ingress routes should be removed") + + _, hasNew := m.routes[types.NamespacedName{Name: "ingress-2", Namespace: "other-ns"}] + assert.True(t, hasNew, "new ingress routes should be present") + }) + + t.Run("filters non-matching ingresses", func(t *testing.T) { + m := newBootstrapIngressManager("api.example.com", func(context.Context) {}) + + ics := []*model.IngressConfig{ + makeIngressConfig("matching", "default", "api.example.com"), + makeIngressConfig("non-matching", "default", "other.example.com"), + } + changes, err := m.Set(t.Context(), ics) + require.NoError(t, err) + assert.True(t, changes) + assert.Len(t, m.routes, 1) + + _, hasMatching := m.routes[types.NamespacedName{Name: "matching", Namespace: "default"}] + assert.True(t, hasMatching) + }) + + t.Run("no changes", func(t *testing.T) { + var updateCalled int + m := newBootstrapIngressManager("api.example.com", func(context.Context) { + updateCalled++ + }) + + ics := []*model.IngressConfig{ + makeIngressConfig("non-matching", "default", "other.example.com"), + } + changes, err := m.Set(t.Context(), ics) + require.NoError(t, err) + assert.False(t, changes) + assert.Equal(t, 0, updateCalled) + }) + }) + + t.Run("Delete", func(t *testing.T) { + t.Run("removes routes for existing ingress", func(t *testing.T) { + var updateCalled int + m := newBootstrapIngressManager("api.example.com", func(context.Context) { + updateCalled++ + }) + + ic := makeIngressConfig("my-ingress", "default", "api.example.com") + _, err := m.Upsert(t.Context(), ic) + require.NoError(t, err) + assert.Equal(t, 1, updateCalled) + assert.Len(t, m.routes, 1) + + changes, err := m.Delete(t.Context(), types.NamespacedName{Name: "my-ingress", Namespace: "default"}) + require.NoError(t, err) + assert.True(t, changes) + assert.Equal(t, 2, updateCalled) + assert.Len(t, m.routes, 0) + }) + + t.Run("no changes when deleting non-existent ingress", func(t *testing.T) { + var updateCalled int + m := newBootstrapIngressManager("api.example.com", func(context.Context) { + updateCalled++ + }) + + changes, err := m.Delete(t.Context(), types.NamespacedName{Name: "non-existent", Namespace: "default"}) + require.NoError(t, err) + assert.False(t, changes) + assert.Equal(t, 0, updateCalled) + }) + }) + + t.Run("addRoutesToConfig", func(t *testing.T) { + t.Run("adds routes from multiple ingresses", func(t *testing.T) { + m := newBootstrapIngressManager("api.example.com", func(context.Context) {}) + + ic1 := makeIngressConfig("ingress-1", "ns1", "api.example.com") + ic2 := makeIngressConfig("ingress-2", "ns2", "api.example.com") + _, err := m.Upsert(t.Context(), ic1) + require.NoError(t, err) + _, err = m.Upsert(t.Context(), ic2) + require.NoError(t, err) + + cfg := &config.Config{Options: &config.Options{}} + m.addRoutesToConfig(cfg) + + assert.Len(t, cfg.Options.Routes, 2) + }) + + t.Run("nil", func(t *testing.T) { + cfg := &config.Config{Options: &config.Options{}} + assert.NotPanics(t, func() { + (*bootstrapIngressManager)(nil).addRoutesToConfig(cfg) + }) + assert.Empty(t, cfg.Options.Routes) + }) + }) +} diff --git a/pomerium/ctrl/config.go b/pomerium/ctrl/config.go index 311f35d7..3f132d71 100644 --- a/pomerium/ctrl/config.go +++ b/pomerium/ctrl/config.go @@ -6,6 +6,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "google.golang.org/protobuf/testing/protocmp" "github.com/pomerium/pomerium/config" ) @@ -23,7 +24,9 @@ var ( var ( cmpOpts = []cmp.Option{ + protocmp.Transform(), cmpopts.IgnoreUnexported(config.Options{}), + cmpopts.IgnoreUnexported(config.Policy{}), cmpopts.EquateEmpty(), } ) diff --git a/pomerium/ctrl/run.go b/pomerium/ctrl/run.go index 758971cb..b0f84445 100644 --- a/pomerium/ctrl/run.go +++ b/pomerium/ctrl/run.go @@ -3,6 +3,7 @@ package ctrl import ( "context" "fmt" + "net/url" "sync" "sigs.k8s.io/controller-runtime/pkg/log" @@ -18,8 +19,11 @@ var _ = pomerium.ConfigReconciler(new(Runner)) // Runner implements pomerium control loop type Runner struct { + *bootstrapIngressManager + src *InMemoryConfigSource base config.Config + cfg config.Config sync.Once ready chan struct{} } @@ -51,21 +55,46 @@ func (r *Runner) SetConfig(ctx context.Context, src *model.Config) (changes bool return false, fmt.Errorf("transform config: %w", err) } - changed := r.src.SetConfig(ctx, dst) + // If using all-in-one mode together with the sync API, we may need to bootstrap + // more of the settings than usual. The Enterprise API can't be used to control + // many of the core settings (authenticate URL, IdP settings, etc.). + if r.bootstrapIngressManager != nil { + if err := ApplyAdditional(ctx, dst.Options, src); err != nil { + return false, fmt.Errorf("additional settings: %w", err) + } + } + + r.cfg = *dst + changed := r.updateConfig(ctx) r.Once.Do(r.readyToRun) return changed, nil } +func (r *Runner) updateConfig(ctx context.Context) (changes bool) { + cfg := r.cfg.Clone() + r.bootstrapIngressManager.addRoutesToConfig(cfg) + return r.src.SetConfig(ctx, cfg) +} + // NewPomeriumRunner creates new pomerium command and control -func NewPomeriumRunner(base config.Config, listener config.ChangeListener) (*Runner, error) { - return &Runner{ +func NewPomeriumRunner(base config.Config, listener config.ChangeListener, syncAPIURL string) (*Runner, error) { + runner := &Runner{ base: base, src: &InMemoryConfigSource{ listeners: []config.ChangeListener{listener}, }, ready: make(chan struct{}), - }, nil + } + if syncAPIURL != "" { + u, err := url.Parse(syncAPIURL) + if err != nil { + return nil, fmt.Errorf("couldn't parse sync API URL: %w", err) + } + runner.bootstrapIngressManager = newBootstrapIngressManager(u.Host, + func(ctx context.Context) { runner.updateConfig(ctx) }) + } + return runner, nil } // Run starts pomerium once config is available diff --git a/pomerium/ingress_to_route.go b/pomerium/ingress_to_route.go index c01c56be..4b58dd48 100644 --- a/pomerium/ingress_to_route.go +++ b/pomerium/ingress_to_route.go @@ -15,12 +15,31 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/pomerium/pomerium/config" pb "github.com/pomerium/pomerium/pkg/grpc/config" "github.com/pomerium/ingress-controller/model" ) -// ingressToRoutes converts Ingress object into Pomerium Route +// IngressToRoutes converts Ingress objects into Pomerium routes (the config +// struct type, not the protobuf type). +func IngressToRoutes(ctx context.Context, ic *model.IngressConfig) ([]config.Policy, error) { + pbRoutes, err := ingressToRoutes(ctx, ic) + if err != nil { + return nil, err + } + routes := make([]config.Policy, len(pbRoutes)) + for i := range pbRoutes { + r, err := config.NewPolicyFromProto(pbRoutes[i]) + if err != nil { + return nil, err + } + routes[i] = *r + } + return routes, nil +} + +// ingressToRoutes converts Ingress object into Pomerium Routes func ingressToRoutes(ctx context.Context, ic *model.IngressConfig) (routeList, error) { tmpl := &pb.Route{} diff --git a/pomerium/sync_api.go b/pomerium/sync_api.go index 8a7c9982..c02a0b02 100644 --- a/pomerium/sync_api.go +++ b/pomerium/sync_api.go @@ -2,9 +2,13 @@ package pomerium import ( "context" + "crypto/tls" "errors" "fmt" "maps" + "net" + "net/http" + "net/url" "slices" "strconv" "strings" @@ -35,13 +39,31 @@ import ( // NewAPIReconciler initializes a reconciler that syncs using the unified API, // for the given API url and API token. func NewAPIReconciler( - url, token string, baseOptions *config.Options, + apiURL, apiToken string, baseOptions *config.Options, dialAddressOverride string, ) Reconciler { - client := sdk.NewClient( - sdk.WithURL(url), - sdk.WithAPIToken(token)) + opts := []sdk.ClientOption{ + sdk.WithURL(apiURL), + sdk.WithAPIToken(apiToken), + } + + if dialAddressOverride != "" { + u, _ := url.Parse(apiURL) + dialer := &tls.Dialer{ + Config: &tls.Config{ + ServerName: u.Hostname(), + }, + } + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.DialTLSContext = func(ctx context.Context, network, _ string) (net.Conn, error) { + return dialer.DialContext(ctx, network, dialAddressOverride) + } + opts = append(opts, sdk.WithHTTPClient(&http.Client{ + Transport: transport, + })) + } + return &APIReconciler{ - apiClient: client, + apiClient: sdk.NewClient(opts...), baseOptions: baseOptions, secretsMap: model.NewTLSSecretsMap(), } @@ -637,12 +659,18 @@ func (r *APIReconciler) upsertOneRoute(ctx context.Context, route *configpb.Rout if err == nil { route.Id = resp.Msg.Route.Id return true, nil - } else if connect.CodeOf(err) != connect.CodeAlreadyExists { + } + // If we already created a route, but failed to save the ID annotation, + // we may get either an already_exists error (there is a name uniqueness + // constraint in Pomerium Zero), or a failed_precondition error (there is + // a route 'From' overlap check in Pomerium Enterprise). Any other error + // should be returned as is. + errCode := connect.CodeOf(err) + if errCode != connect.CodeAlreadyExists && errCode != connect.CodeFailedPrecondition { return false, err } - // If we already created a route, but failed to save the ID annotation, - // attempt to look up the route by name. + // Attempt to look up the route by name. existing, err = r.findRouteByName(ctx, route.GetName()) if err != nil { return false, err