package state import ( "testing" "time" memdb "github.com/hashicorp/go-memdb" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/sdk/testutil" ) func TestStore_ConfigEntry(t *testing.T) { require := require.New(t) s := testConfigStateStore(t) expected := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "global", Config: map[string]interface{}{ "DestinationServiceName": "foo", }, } // Create require.NoError(s.EnsureConfigEntry(0, expected)) idx, config, err := s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(0), idx) require.Equal(expected, config) // Update updated := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "global", Config: map[string]interface{}{ "DestinationServiceName": "bar", }, } require.NoError(s.EnsureConfigEntry(1, updated)) idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(1), idx) require.Equal(updated, config) // Delete require.NoError(s.DeleteConfigEntry(2, structs.ProxyDefaults, "global", nil)) idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(2), idx) require.Nil(config) // Set up a watch. serviceConf := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", } require.NoError(s.EnsureConfigEntry(3, serviceConf)) ws := memdb.NewWatchSet() _, _, err = s.ConfigEntry(ws, structs.ServiceDefaults, "foo", nil) require.NoError(err) // Make an unrelated modification and make sure the watch doesn't fire. require.NoError(s.EnsureConfigEntry(4, updated)) require.False(watchFired(ws)) // Update the watched config and make sure it fires. serviceConf.Protocol = "http" require.NoError(s.EnsureConfigEntry(5, serviceConf)) require.True(watchFired(ws)) } func TestStore_ConfigEntryCAS(t *testing.T) { require := require.New(t) s := testConfigStateStore(t) expected := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "global", Config: map[string]interface{}{ "DestinationServiceName": "foo", }, } // Create require.NoError(s.EnsureConfigEntry(1, expected)) idx, config, err := s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(1), idx) require.Equal(expected, config) // Update with invalid index updated := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "global", Config: map[string]interface{}{ "DestinationServiceName": "bar", }, } ok, err := s.EnsureConfigEntryCAS(2, 99, updated) require.False(ok) require.NoError(err) // Entry should not be changed idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(1), idx) require.Equal(expected, config) // Update with a valid index ok, err = s.EnsureConfigEntryCAS(2, 1, updated) require.True(ok) require.NoError(err) // Entry should be updated idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(err) require.Equal(uint64(2), idx) require.Equal(updated, config) } func TestStore_ConfigEntry_UpdateOver(t *testing.T) { // This test uses ServiceIntentions because they are the only // kind that implements UpdateOver() at this time. s := testConfigStateStore(t) var ( idA = testUUID() idB = testUUID() loc = time.FixedZone("UTC-8", -8*60*60) timeA = time.Date(1955, 11, 5, 6, 15, 0, 0, loc) timeB = time.Date(1985, 10, 26, 1, 35, 0, 0, loc) ) require.NotEqual(t, idA, idB) initial := &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ { LegacyID: idA, Name: "web", Action: structs.IntentionActionAllow, LegacyCreateTime: &timeA, LegacyUpdateTime: &timeA, }, }, } // Create nextIndex := uint64(1) require.NoError(t, s.EnsureConfigEntry(nextIndex, initial.Clone())) idx, raw, err := s.ConfigEntry(nil, structs.ServiceIntentions, "api", nil) require.NoError(t, err) require.Equal(t, nextIndex, idx) got, ok := raw.(*structs.ServiceIntentionsConfigEntry) require.True(t, ok) initial.RaftIndex = got.RaftIndex require.Equal(t, initial, got) t.Run("update and fail change legacyID", func(t *testing.T) { // Update updated := &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ { LegacyID: idB, Name: "web", Action: structs.IntentionActionDeny, LegacyCreateTime: &timeB, LegacyUpdateTime: &timeB, }, }, } nextIndex++ err := s.EnsureConfigEntry(nextIndex, updated.Clone()) testutil.RequireErrorContains(t, err, "cannot set this field to a different value") }) t.Run("update and do not update create time", func(t *testing.T) { // Update updated := &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ { LegacyID: idA, Name: "web", Action: structs.IntentionActionDeny, LegacyCreateTime: &timeB, LegacyUpdateTime: &timeB, }, }, } nextIndex++ require.NoError(t, s.EnsureConfigEntry(nextIndex, updated.Clone())) // check idx, raw, err = s.ConfigEntry(nil, structs.ServiceIntentions, "api", nil) require.NoError(t, err) require.Equal(t, nextIndex, idx) got, ok = raw.(*structs.ServiceIntentionsConfigEntry) require.True(t, ok) updated.RaftIndex = got.RaftIndex updated.Sources[0].LegacyCreateTime = &timeA // UpdateOver will not replace this require.Equal(t, updated, got) }) } func TestStore_ConfigEntries(t *testing.T) { require := require.New(t) s := testConfigStateStore(t) // Create some config entries. entry1 := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "test1", } entry2 := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "test2", } entry3 := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "test3", } require.NoError(s.EnsureConfigEntry(0, entry1)) require.NoError(s.EnsureConfigEntry(1, entry2)) require.NoError(s.EnsureConfigEntry(2, entry3)) // Get all entries idx, entries, err := s.ConfigEntries(nil, nil) require.NoError(err) require.Equal(uint64(2), idx) require.Equal([]structs.ConfigEntry{entry1, entry2, entry3}, entries) // Get all proxy entries idx, entries, err = s.ConfigEntriesByKind(nil, structs.ProxyDefaults, nil) require.NoError(err) require.Equal(uint64(2), idx) require.Equal([]structs.ConfigEntry{entry1}, entries) // Get all service entries ws := memdb.NewWatchSet() idx, entries, err = s.ConfigEntriesByKind(ws, structs.ServiceDefaults, nil) require.NoError(err) require.Equal(uint64(2), idx) require.Equal([]structs.ConfigEntry{entry2, entry3}, entries) // Watch should not have fired require.False(watchFired(ws)) // Now make an update and make sure the watch fires. require.NoError(s.EnsureConfigEntry(3, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "test2", Protocol: "tcp", })) require.True(watchFired(ws)) } func TestStore_ConfigEntry_GraphValidation(t *testing.T) { type tcase struct { entries []structs.ConfigEntry op func(t *testing.T, s *Store) error expectErr string expectGraphErr bool } cases := map[string]tcase{ "splitter fails without default protocol": { entries: []structs.ConfigEntry{}, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, } return s.EnsureConfigEntry(0, entry) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, "splitter fails with tcp protocol": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, } return s.EnsureConfigEntry(0, entry) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, "splitter works with http protocol": { entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "tcp", // loses }, }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", EnterpriseMeta: *structs.DefaultEnterpriseMeta(), }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == v1", }, "v2": { Filter: "Service.Meta.version == v2", }, }, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90, ServiceSubset: "v1"}, {Weight: 10, ServiceSubset: "v2"}, }, EnterpriseMeta: *structs.DefaultEnterpriseMeta(), } return s.EnsureConfigEntry(0, entry) }, }, "splitter works with http protocol (from proxy-defaults)": { entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == v1", }, "v2": { Filter: "Service.Meta.version == v2", }, }, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90, ServiceSubset: "v1"}, {Weight: 10, ServiceSubset: "v2"}, }, } return s.EnsureConfigEntry(0, entry) }, }, "router fails with tcp protocol": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "other": { Filter: "Service.Meta.version == other", }, }, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/other", }, }, Destination: &structs.ServiceRouteDestination{ ServiceSubset: "other", }, }, }, } return s.EnsureConfigEntry(0, entry) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, "router fails without default protocol": { entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "other": { Filter: "Service.Meta.version == other", }, }, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/other", }, }, Destination: &structs.ServiceRouteDestination{ ServiceSubset: "other", }, }, }, } return s.EnsureConfigEntry(0, entry) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, ///////////////////////////////////////////////// "cannot remove default protocol after splitter created": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == v1", }, "v2": { Filter: "Service.Meta.version == v2", }, }, }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90, ServiceSubset: "v1"}, {Weight: 10, ServiceSubset: "v2"}, }, }, }, op: func(t *testing.T, s *Store) error { return s.DeleteConfigEntry(0, structs.ServiceDefaults, "main", nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, "cannot remove global default protocol after splitter created": { entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == v1", }, "v2": { Filter: "Service.Meta.version == v2", }, }, }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90, ServiceSubset: "v1"}, {Weight: 10, ServiceSubset: "v2"}, }, }, }, op: func(t *testing.T, s *Store) error { return s.DeleteConfigEntry(0, structs.ProxyDefaults, structs.ProxyConfigGlobal, nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, "can remove global default protocol after splitter created if service default overrides it": { entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == v1", }, "v2": { Filter: "Service.Meta.version == v2", }, }, }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90, ServiceSubset: "v1"}, {Weight: 10, ServiceSubset: "v2"}, }, }, }, op: func(t *testing.T, s *Store) error { return s.DeleteConfigEntry(0, structs.ProxyDefaults, structs.ProxyConfigGlobal, nil) }, }, "cannot change to tcp protocol after splitter created": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == v1", }, "v2": { Filter: "Service.Meta.version == v2", }, }, }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90, ServiceSubset: "v1"}, {Weight: 10, ServiceSubset: "v2"}, }, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", } return s.EnsureConfigEntry(0, entry) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, "cannot remove default protocol after router created": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "other": { Filter: "Service.Meta.version == other", }, }, }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/other", }, }, Destination: &structs.ServiceRouteDestination{ ServiceSubset: "other", }, }, }, }, }, op: func(t *testing.T, s *Store) error { return s.DeleteConfigEntry(0, structs.ServiceDefaults, "main", nil) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, "cannot change to tcp protocol after router created": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "other": { Filter: "Service.Meta.version == other", }, }, }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/other", }, }, Destination: &structs.ServiceRouteDestination{ ServiceSubset: "other", }, }, }, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", } return s.EnsureConfigEntry(0, entry) }, expectErr: "does not permit advanced routing or splitting behavior", expectGraphErr: true, }, ///////////////////////////////////////////////// "cannot split to a service using tcp": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "other", Protocol: "tcp", }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90}, {Weight: 10, Service: "other"}, }, } return s.EnsureConfigEntry(0, entry) }, expectErr: "uses inconsistent protocols", expectGraphErr: true, }, "cannot route to a service using tcp": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "other", Protocol: "tcp", }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/other", }, }, Destination: &structs.ServiceRouteDestination{ Service: "other", }, }, }, } return s.EnsureConfigEntry(0, entry) }, expectErr: "uses inconsistent protocols", expectGraphErr: true, }, ///////////////////////////////////////////////// "cannot failover to a service using a different protocol": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "grpc", }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "other", Protocol: "tcp", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", ConnectTimeout: 33 * time.Second, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": { Service: "other", }, }, } return s.EnsureConfigEntry(0, entry) }, expectErr: "uses inconsistent protocols", expectGraphErr: true, }, "cannot redirect to a service using a different protocol": { entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "grpc", }, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "other", Protocol: "tcp", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", ConnectTimeout: 33 * time.Second, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, } return s.EnsureConfigEntry(0, entry) }, expectErr: "uses inconsistent protocols", expectGraphErr: true, }, ///////////////////////////////////////////////// "redirect to a subset that does exist is fine": { entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "other", ConnectTimeout: 33 * time.Second, Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == v1", }, }, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", ServiceSubset: "v1", }, } return s.EnsureConfigEntry(0, entry) }, }, "cannot redirect to a subset that does not exist": { entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "other", ConnectTimeout: 33 * time.Second, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", ServiceSubset: "v1", }, } return s.EnsureConfigEntry(0, entry) }, expectErr: `does not have a subset named "v1"`, expectGraphErr: true, }, ///////////////////////////////////////////////// "cannot introduce circular resolver redirect": { entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "other", Redirect: &structs.ServiceResolverRedirect{ Service: "main", }, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, } return s.EnsureConfigEntry(0, entry) }, expectErr: `detected circular resolver redirect`, expectGraphErr: true, }, "cannot introduce circular split": { entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "other", Splits: []structs.ServiceSplit{ {Weight: 100, Service: "main"}, }, }, }, op: func(t *testing.T, s *Store) error { entry := &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100, Service: "other"}, }, } return s.EnsureConfigEntry(0, entry) }, expectErr: `detected circular reference`, expectGraphErr: true, }, } for name, tc := range cases { name := name tc := tc t.Run(name, func(t *testing.T) { s := testConfigStateStore(t) for _, entry := range tc.entries { require.NoError(t, entry.Normalize()) require.NoError(t, s.EnsureConfigEntry(0, entry)) } err := tc.op(t, s) if tc.expectErr != "" { require.Error(t, err) require.Contains(t, err.Error(), tc.expectErr) _, ok := err.(*structs.ConfigEntryGraphError) if tc.expectGraphErr { require.True(t, ok, "%T is not a *ConfigEntryGraphError", err) } else { require.False(t, ok, "did not expect a *ConfigEntryGraphError here: %v", err) } } else { require.NoError(t, err) } }) } } func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) { for _, tc := range []struct { name string entries []structs.ConfigEntry expectBefore []ConfigEntryKindName overrides map[ConfigEntryKindName]structs.ConfigEntry expectAfter []ConfigEntryKindName expectAfterErr string checkAfter func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) }{ { name: "mask service-defaults", entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", }, }, expectBefore: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), }, overrides: map[ConfigEntryKindName]structs.ConfigEntry{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil): nil, }, expectAfter: []ConfigEntryKindName{ // nothing }, }, { name: "edit service-defaults", entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", }, }, expectBefore: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), }, overrides: map[ConfigEntryKindName]structs.ConfigEntry{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil): &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "grpc", }, }, expectAfter: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), }, checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { defaults := entrySet.GetService(structs.NewServiceID("main", nil)) require.NotNil(t, defaults) require.Equal(t, "grpc", defaults.Protocol) }, }, { name: "mask service-router", entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", }, }, expectBefore: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), NewConfigEntryKindName(structs.ServiceRouter, "main", nil), }, overrides: map[ConfigEntryKindName]structs.ConfigEntry{ NewConfigEntryKindName(structs.ServiceRouter, "main", nil): nil, }, expectAfter: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), }, }, { name: "edit service-router", entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": {Filter: "Service.Meta.version == v1"}, "v2": {Filter: "Service.Meta.version == v2"}, "v3": {Filter: "Service.Meta.version == v3"}, }, }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/admin", }, }, Destination: &structs.ServiceRouteDestination{ ServiceSubset: "v2", }, }, }, }, }, expectBefore: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), NewConfigEntryKindName(structs.ServiceResolver, "main", nil), NewConfigEntryKindName(structs.ServiceRouter, "main", nil), }, overrides: map[ConfigEntryKindName]structs.ConfigEntry{ NewConfigEntryKindName(structs.ServiceRouter, "main", nil): &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/admin", }, }, Destination: &structs.ServiceRouteDestination{ ServiceSubset: "v3", }, }, }, }, }, expectAfter: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), NewConfigEntryKindName(structs.ServiceResolver, "main", nil), NewConfigEntryKindName(structs.ServiceRouter, "main", nil), }, checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { router := entrySet.GetRouter(structs.NewServiceID("main", nil)) require.NotNil(t, router) require.Len(t, router.Routes, 1) expect := structs.ServiceRoute{ Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/admin", }, }, Destination: &structs.ServiceRouteDestination{ ServiceSubset: "v3", }, } require.Equal(t, expect, router.Routes[0]) }, }, { name: "mask service-splitter", entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, }, expectBefore: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), NewConfigEntryKindName(structs.ServiceSplitter, "main", nil), }, overrides: map[ConfigEntryKindName]structs.ConfigEntry{ NewConfigEntryKindName(structs.ServiceSplitter, "main", nil): nil, }, expectAfter: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), }, }, { name: "edit service-splitter", entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, }, expectBefore: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), NewConfigEntryKindName(structs.ServiceSplitter, "main", nil), }, overrides: map[ConfigEntryKindName]structs.ConfigEntry{ NewConfigEntryKindName(structs.ServiceSplitter, "main", nil): &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 85, ServiceSubset: "v1"}, {Weight: 15, ServiceSubset: "v2"}, }, }, }, expectAfter: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceDefaults, "main", nil), NewConfigEntryKindName(structs.ServiceSplitter, "main", nil), }, checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { splitter := entrySet.GetSplitter(structs.NewServiceID("main", nil)) require.NotNil(t, splitter) require.Len(t, splitter.Splits, 2) expect := []structs.ServiceSplit{ {Weight: 85, ServiceSubset: "v1"}, {Weight: 15, ServiceSubset: "v2"}, } require.Equal(t, expect, splitter.Splits) }, }, { name: "mask service-resolver", entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", }, }, expectBefore: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceResolver, "main", nil), }, overrides: map[ConfigEntryKindName]structs.ConfigEntry{ NewConfigEntryKindName(structs.ServiceResolver, "main", nil): nil, }, expectAfter: []ConfigEntryKindName{ // nothing }, }, { name: "edit service-resolver", entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", }, }, expectBefore: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceResolver, "main", nil), }, overrides: map[ConfigEntryKindName]structs.ConfigEntry{ NewConfigEntryKindName(structs.ServiceResolver, "main", nil): &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", ConnectTimeout: 33 * time.Second, }, }, expectAfter: []ConfigEntryKindName{ NewConfigEntryKindName(structs.ServiceResolver, "main", nil), }, checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { resolver := entrySet.GetResolver(structs.NewServiceID("main", nil)) require.NotNil(t, resolver) require.Equal(t, 33*time.Second, resolver.ConnectTimeout) }, }, } { tc := tc t.Run(tc.name, func(t *testing.T) { s := testConfigStateStore(t) for _, entry := range tc.entries { require.NoError(t, s.EnsureConfigEntry(0, entry)) } t.Run("without override", func(t *testing.T) { _, entrySet, err := s.readDiscoveryChainConfigEntries(nil, "main", nil, nil) require.NoError(t, err) got := entrySetToKindNames(entrySet) require.ElementsMatch(t, tc.expectBefore, got) }) t.Run("with override", func(t *testing.T) { _, entrySet, err := s.readDiscoveryChainConfigEntries(nil, "main", tc.overrides, nil) if tc.expectAfterErr != "" { require.Error(t, err) require.Contains(t, err.Error(), tc.expectAfterErr) } else { require.NoError(t, err) got := entrySetToKindNames(entrySet) require.ElementsMatch(t, tc.expectAfter, got) if tc.checkAfter != nil { tc.checkAfter(t, entrySet) } } }) }) } } func entrySetToKindNames(entrySet *structs.DiscoveryChainConfigEntries) []ConfigEntryKindName { var out []ConfigEntryKindName for _, entry := range entrySet.Routers { out = append(out, NewConfigEntryKindName( entry.Kind, entry.Name, &entry.EnterpriseMeta, )) } for _, entry := range entrySet.Splitters { out = append(out, NewConfigEntryKindName( entry.Kind, entry.Name, &entry.EnterpriseMeta, )) } for _, entry := range entrySet.Resolvers { out = append(out, NewConfigEntryKindName( entry.Kind, entry.Name, &entry.EnterpriseMeta, )) } for _, entry := range entrySet.Services { out = append(out, NewConfigEntryKindName( entry.Kind, entry.Name, &entry.EnterpriseMeta, )) } return out } func TestStore_ReadDiscoveryChainConfigEntries_SubsetSplit(t *testing.T) { s := testConfigStateStore(t) entries := []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "http", }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == v1", }, "v2": { Filter: "Service.Meta.version == v2", }, }, }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90, ServiceSubset: "v1"}, {Weight: 10, ServiceSubset: "v2"}, }, }, } for _, entry := range entries { require.NoError(t, s.EnsureConfigEntry(0, entry)) } _, entrySet, err := s.readDiscoveryChainConfigEntries(nil, "main", nil, nil) require.NoError(t, err) require.Len(t, entrySet.Routers, 0) require.Len(t, entrySet.Splitters, 1) require.Len(t, entrySet.Resolvers, 1) require.Len(t, entrySet.Services, 1) } // TODO(rb): add ServiceIntentions tests func TestStore_ValidateGatewayNamesCannotBeShared(t *testing.T) { s := testConfigStateStore(t) ingress := &structs.IngressGatewayConfigEntry{ Kind: structs.IngressGateway, Name: "gateway", } require.NoError(t, s.EnsureConfigEntry(0, ingress)) terminating := &structs.TerminatingGatewayConfigEntry{ Kind: structs.TerminatingGateway, Name: "gateway", } // Cannot have 2 gateways with same service name require.Error(t, s.EnsureConfigEntry(1, terminating)) ingress = &structs.IngressGatewayConfigEntry{ Kind: structs.IngressGateway, Name: "gateway", Listeners: []structs.IngressListener{ {Port: 8080}, }, } require.NoError(t, s.EnsureConfigEntry(2, ingress)) require.NoError(t, s.DeleteConfigEntry(3, structs.IngressGateway, "gateway", nil)) // Adding the terminating gateway with same name should now work require.NoError(t, s.EnsureConfigEntry(4, terminating)) // Cannot have 2 gateways with same service name require.Error(t, s.EnsureConfigEntry(5, ingress)) } func TestStore_ValidateIngressGatewayErrorOnMismatchedProtocols(t *testing.T) { newIngress := func(protocol, name string) *structs.IngressGatewayConfigEntry { return &structs.IngressGatewayConfigEntry{ Kind: structs.IngressGateway, Name: "gateway", Listeners: []structs.IngressListener{ { Port: 8080, Protocol: protocol, Services: []structs.IngressService{ {Name: name}, }, }, }, } } t.Run("http ingress fails with http upstream later changed to tcp", func(t *testing.T) { s := testConfigStateStore(t) // First set the target service as http expected := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Protocol: "http", } require.NoError(t, s.EnsureConfigEntry(0, expected)) // Next configure http ingress to route to the http service require.NoError(t, s.EnsureConfigEntry(1, newIngress("http", "web"))) t.Run("via modification", func(t *testing.T) { // Now redefine the target service as tcp expected = &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Protocol: "tcp", } err := s.EnsureConfigEntry(2, expected) require.Error(t, err) require.Contains(t, err.Error(), `has protocol "tcp"`) }) t.Run("via deletion", func(t *testing.T) { // This will fall back to the default tcp. err := s.DeleteConfigEntry(2, structs.ServiceDefaults, "web", nil) require.Error(t, err) require.Contains(t, err.Error(), `has protocol "tcp"`) }) }) t.Run("tcp ingress ok with tcp upstream (defaulted) later changed to http", func(t *testing.T) { s := testConfigStateStore(t) // First configure tcp ingress to route to a defaulted tcp service require.NoError(t, s.EnsureConfigEntry(0, newIngress("tcp", "web"))) // Now redefine the target service as http expected := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Protocol: "http", } require.NoError(t, s.EnsureConfigEntry(1, expected)) }) t.Run("tcp ingress fails with tcp upstream (defaulted) later changed to http", func(t *testing.T) { s := testConfigStateStore(t) // First configure tcp ingress to route to a defaulted tcp service require.NoError(t, s.EnsureConfigEntry(0, newIngress("tcp", "web"))) // Now redefine the target service as http expected := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Protocol: "http", } require.NoError(t, s.EnsureConfigEntry(1, expected)) t.Run("and a router defined", func(t *testing.T) { // This part should fail. expected2 := &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "web", } err := s.EnsureConfigEntry(2, expected2) require.Error(t, err) require.Contains(t, err.Error(), `has protocol "http"`) }) t.Run("and a splitter defined", func(t *testing.T) { // This part should fail. expected2 := &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "web", Splits: []structs.ServiceSplit{ {Weight: 100}, }, } err := s.EnsureConfigEntry(2, expected2) require.Error(t, err) require.Contains(t, err.Error(), `has protocol "http"`) }) }) t.Run("http ingress fails with tcp upstream (defaulted)", func(t *testing.T) { s := testConfigStateStore(t) err := s.EnsureConfigEntry(0, newIngress("http", "web")) require.Error(t, err) require.Contains(t, err.Error(), `has protocol "tcp"`) }) t.Run("http ingress fails with http2 upstream (via proxy-defaults)", func(t *testing.T) { s := testConfigStateStore(t) expected := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "global", Config: map[string]interface{}{ "protocol": "http2", }, } require.NoError(t, s.EnsureConfigEntry(0, expected)) err := s.EnsureConfigEntry(1, newIngress("http", "web")) require.Error(t, err) require.Contains(t, err.Error(), `has protocol "http2"`) }) t.Run("http ingress fails with grpc upstream (via service-defaults)", func(t *testing.T) { s := testConfigStateStore(t) expected := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Protocol: "grpc", } require.NoError(t, s.EnsureConfigEntry(1, expected)) err := s.EnsureConfigEntry(2, newIngress("http", "web")) require.Error(t, err) require.Contains(t, err.Error(), `has protocol "grpc"`) }) t.Run("http ingress ok with http upstream (via service-defaults)", func(t *testing.T) { s := testConfigStateStore(t) expected := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Protocol: "http", } require.NoError(t, s.EnsureConfigEntry(2, expected)) require.NoError(t, s.EnsureConfigEntry(3, newIngress("http", "web"))) }) t.Run("http ingress ignores wildcard specifier", func(t *testing.T) { s := testConfigStateStore(t) require.NoError(t, s.EnsureConfigEntry(4, newIngress("http", "*"))) }) t.Run("deleting ingress config entry ok", func(t *testing.T) { s := testConfigStateStore(t) require.NoError(t, s.EnsureConfigEntry(1, newIngress("tcp", "web"))) require.NoError(t, s.DeleteConfigEntry(5, structs.IngressGateway, "gateway", nil)) }) } func TestSourcesForTarget(t *testing.T) { defaultMeta := *structs.DefaultEnterpriseMeta() type expect struct { idx uint64 names []structs.ServiceName } tt := []struct { name string entries []structs.ConfigEntry expect expect }{ { name: "no relevant config entries", entries: []structs.ConfigEntry{}, expect: expect{ idx: 1, names: []structs.ServiceName{ {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, { name: "from route match", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "web", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/sink", }, }, Destination: &structs.ServiceRouteDestination{ Service: "sink", }, }, }, }, }, expect: expect{ idx: 2, names: []structs.ServiceName{ {Name: "web", EnterpriseMeta: defaultMeta}, {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, { name: "from redirect", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "web", Redirect: &structs.ServiceResolverRedirect{ Service: "sink", }, }, }, expect: expect{ idx: 2, names: []structs.ServiceName{ {Name: "web", EnterpriseMeta: defaultMeta}, {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, { name: "from failover", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "web", Failover: map[string]structs.ServiceResolverFailover{ "*": { Service: "sink", Datacenters: []string{"dc2", "dc3"}, }, }, }, }, expect: expect{ idx: 2, names: []structs.ServiceName{ {Name: "web", EnterpriseMeta: defaultMeta}, {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, { name: "from splitter", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "web", Splits: []structs.ServiceSplit{ {Weight: 90, Service: "web"}, {Weight: 10, Service: "sink"}, }, }, }, expect: expect{ idx: 2, names: []structs.ServiceName{ {Name: "web", EnterpriseMeta: defaultMeta}, {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, { name: "chained route redirect", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "source", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/route", }, }, Destination: &structs.ServiceRouteDestination{ Service: "routed", }, }, }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "routed", Redirect: &structs.ServiceResolverRedirect{ Service: "sink", }, }, }, expect: expect{ idx: 3, names: []structs.ServiceName{ {Name: "source", EnterpriseMeta: defaultMeta}, {Name: "routed", EnterpriseMeta: defaultMeta}, {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, { name: "kitchen sink with multiple services referencing sink directly", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "routed", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/sink", }, }, Destination: &structs.ServiceRouteDestination{ Service: "sink", }, }, }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "redirected", Redirect: &structs.ServiceResolverRedirect{ Service: "sink", }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "failed-over", Failover: map[string]structs.ServiceResolverFailover{ "*": { Service: "sink", Datacenters: []string{"dc2", "dc3"}, }, }, }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "split", Splits: []structs.ServiceSplit{ {Weight: 90, Service: "no-op"}, {Weight: 10, Service: "sink"}, }, }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "unrelated", Splits: []structs.ServiceSplit{ {Weight: 90, Service: "zip"}, {Weight: 10, Service: "zop"}, }, }, }, expect: expect{ idx: 6, names: []structs.ServiceName{ {Name: "split", EnterpriseMeta: defaultMeta}, {Name: "failed-over", EnterpriseMeta: defaultMeta}, {Name: "redirected", EnterpriseMeta: defaultMeta}, {Name: "routed", EnterpriseMeta: defaultMeta}, {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { s := testStateStore(t) ws := memdb.NewWatchSet() ca := &structs.CAConfiguration{ Provider: "consul", } err := s.CASetConfig(0, ca) require.NoError(t, err) var i uint64 = 1 for _, entry := range tc.entries { require.NoError(t, entry.Normalize()) require.NoError(t, s.EnsureConfigEntry(i, entry)) i++ } tx := s.db.ReadTxn() defer tx.Abort() sn := structs.NewServiceName("sink", structs.DefaultEnterpriseMeta()) idx, names, err := s.discoveryChainSourcesTxn(tx, ws, "dc1", sn) require.NoError(t, err) require.Equal(t, tc.expect.idx, idx) require.ElementsMatch(t, tc.expect.names, names) }) } } func TestTargetsForSource(t *testing.T) { defaultMeta := *structs.DefaultEnterpriseMeta() type expect struct { idx uint64 ids []structs.ServiceName } tt := []struct { name string entries []structs.ConfigEntry expect expect }{ { name: "from route match", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "web", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/sink", }, }, Destination: &structs.ServiceRouteDestination{ Service: "sink", }, }, }, }, }, expect: expect{ idx: 2, ids: []structs.ServiceName{ {Name: "web", EnterpriseMeta: defaultMeta}, {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, { name: "from redirect", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "web", Redirect: &structs.ServiceResolverRedirect{ Service: "sink", }, }, }, expect: expect{ idx: 2, ids: []structs.ServiceName{ {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, { name: "from failover", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "web", Failover: map[string]structs.ServiceResolverFailover{ "*": { Service: "remote-web", Datacenters: []string{"dc2", "dc3"}, }, }, }, }, expect: expect{ idx: 2, ids: []structs.ServiceName{ {Name: "web", EnterpriseMeta: defaultMeta}, }, }, }, { name: "from splitter", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "web", Splits: []structs.ServiceSplit{ {Weight: 90, Service: "web"}, {Weight: 10, Service: "sink"}, }, }, }, expect: expect{ idx: 2, ids: []structs.ServiceName{ {Name: "web", EnterpriseMeta: defaultMeta}, {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, { name: "chained route redirect", entries: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, }, &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "web", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/route", }, }, Destination: &structs.ServiceRouteDestination{ Service: "routed", }, }, }, }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "routed", Redirect: &structs.ServiceResolverRedirect{ Service: "sink", }, }, }, expect: expect{ idx: 3, ids: []structs.ServiceName{ {Name: "web", EnterpriseMeta: defaultMeta}, {Name: "sink", EnterpriseMeta: defaultMeta}, }, }, }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { s := testStateStore(t) ws := memdb.NewWatchSet() ca := &structs.CAConfiguration{ Provider: "consul", } err := s.CASetConfig(0, ca) require.NoError(t, err) var i uint64 = 1 for _, entry := range tc.entries { require.NoError(t, entry.Normalize()) require.NoError(t, s.EnsureConfigEntry(i, entry)) i++ } tx := s.db.ReadTxn() defer tx.Abort() idx, ids, err := s.discoveryChainTargetsTxn(tx, ws, "dc1", "web", nil) require.NoError(t, err) require.Equal(t, tc.expect.idx, idx) require.ElementsMatch(t, tc.expect.ids, ids) }) } } func TestStore_ValidateServiceIntentionsErrorOnIncompatibleProtocols(t *testing.T) { l7perms := []*structs.IntentionPermission{ { Action: structs.IntentionActionAllow, HTTP: &structs.IntentionHTTPPermission{ PathPrefix: "/v2/", }, }, } serviceDefaults := func(service, protocol string) *structs.ServiceConfigEntry { return &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: service, Protocol: protocol, } } proxyDefaults := func(protocol string) *structs.ProxyConfigEntry { return &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": protocol, }, } } type operation struct { entry structs.ConfigEntry deletion bool } type testcase struct { ops []operation expectLastErr string } cases := map[string]testcase{ "L4 intention cannot upgrade to L7 when tcp": { ops: []operation{ { // set the target service as tcp entry: serviceDefaults("api", "tcp"), }, { // create an L4 intention entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Action: structs.IntentionActionAllow}, }, }, }, { // Should fail if converted to L7 entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Permissions: l7perms}, }, }, }, }, expectLastErr: `has protocol "tcp"`, }, "L4 intention can upgrade to L7 when made http via service-defaults": { ops: []operation{ { // set the target service as tcp entry: serviceDefaults("api", "tcp"), }, { // create an L4 intention entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Action: structs.IntentionActionAllow}, }, }, }, { // set the target service as http entry: serviceDefaults("api", "http"), }, { // Should succeed if converted to L7 entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Permissions: l7perms}, }, }, }, }, }, "L4 intention can upgrade to L7 when made http via proxy-defaults": { ops: []operation{ { // set the target service as tcp entry: proxyDefaults("tcp"), }, { // create an L4 intention entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Action: structs.IntentionActionAllow}, }, }, }, { // set the target service as http entry: proxyDefaults("http"), }, { // Should succeed if converted to L7 entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Permissions: l7perms}, }, }, }, }, }, "L7 intention cannot have protocol downgraded to tcp via modification via service-defaults": { ops: []operation{ { // set the target service as http entry: serviceDefaults("api", "http"), }, { // create an L7 intention entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Permissions: l7perms}, }, }, }, { // setting the target service as tcp should fail entry: serviceDefaults("api", "tcp"), }, }, expectLastErr: `has protocol "tcp"`, }, "L7 intention cannot have protocol downgraded to tcp via modification via proxy-defaults": { ops: []operation{ { // set the target service as http entry: proxyDefaults("http"), }, { // create an L7 intention entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Permissions: l7perms}, }, }, }, { // setting the target service as tcp should fail entry: proxyDefaults("tcp"), }, }, expectLastErr: `has protocol "tcp"`, }, "L7 intention cannot have protocol downgraded to tcp via deletion of service-defaults": { ops: []operation{ { // set the target service as http entry: serviceDefaults("api", "http"), }, { // create an L7 intention entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Permissions: l7perms}, }, }, }, { // setting the target service as tcp should fail entry: serviceDefaults("api", "tcp"), deletion: true, }, }, expectLastErr: `has protocol "tcp"`, }, "L7 intention cannot have protocol downgraded to tcp via deletion of proxy-defaults": { ops: []operation{ { // set the target service as http entry: proxyDefaults("http"), }, { // create an L7 intention entry: &structs.ServiceIntentionsConfigEntry{ Kind: structs.ServiceIntentions, Name: "api", Sources: []*structs.SourceIntention{ {Name: "web", Permissions: l7perms}, }, }, }, { // setting the target service as tcp should fail entry: proxyDefaults("tcp"), deletion: true, }, }, expectLastErr: `has protocol "tcp"`, }, } for name, tc := range cases { tc := tc t.Run(name, func(t *testing.T) { s := testStateStore(t) var nextIndex = uint64(1) for i, op := range tc.ops { isLast := (i == len(tc.ops)-1) var err error if op.deletion { err = s.DeleteConfigEntry(nextIndex, op.entry.GetKind(), op.entry.GetName(), nil) } else { err = s.EnsureConfigEntry(nextIndex, op.entry) } if isLast && tc.expectLastErr != "" { testutil.RequireErrorContains(t, err, `has protocol "tcp"`) } else { require.NoError(t, err) } } }) } }