package state import ( "testing" "time" memdb "github.com/hashicorp/go-memdb" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/sdk/testutil" ) func TestStore_ConfigEntry(t *testing.T) { s := testConfigStateStore(t) expected := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "global", Config: map[string]interface{}{ "DestinationServiceName": "foo", }, } // Create require.NoError(t, s.EnsureConfigEntry(0, expected)) idx, config, err := s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(t, err) require.Equal(t, uint64(0), idx) require.Equal(t, expected, config) // Update updated := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "global", Config: map[string]interface{}{ "DestinationServiceName": "bar", }, } require.NoError(t, s.EnsureConfigEntry(1, updated)) idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(t, err) require.Equal(t, uint64(1), idx) require.Equal(t, updated, config) // Delete require.NoError(t, s.DeleteConfigEntry(2, structs.ProxyDefaults, "global", nil)) idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(t, err) require.Equal(t, uint64(2), idx) require.Nil(t, config) // Set up a watch. serviceConf := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "foo", } require.NoError(t, s.EnsureConfigEntry(3, serviceConf)) ws := memdb.NewWatchSet() _, _, err = s.ConfigEntry(ws, structs.ServiceDefaults, "foo", nil) require.NoError(t, err) // Make an unrelated modification and make sure the watch doesn't fire. require.NoError(t, s.EnsureConfigEntry(4, updated)) require.False(t, watchFired(ws)) // Update the watched config and make sure it fires. serviceConf.Protocol = "http" require.NoError(t, s.EnsureConfigEntry(5, serviceConf)) require.True(t, watchFired(ws)) } func TestStore_ConfigEntryCAS(t *testing.T) { s := testConfigStateStore(t) expected := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "global", Config: map[string]interface{}{ "DestinationServiceName": "foo", }, } // Create require.NoError(t, s.EnsureConfigEntry(1, expected)) idx, config, err := s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(t, err) require.Equal(t, uint64(1), idx) require.Equal(t, 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(t, ok) require.NoError(t, err) // Entry should not be changed idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(t, err) require.Equal(t, uint64(1), idx) require.Equal(t, expected, config) // Update with a valid index ok, err = s.EnsureConfigEntryCAS(2, 1, updated) require.True(t, ok) require.NoError(t, err) // Entry should be updated idx, config, err = s.ConfigEntry(nil, structs.ProxyDefaults, "global", nil) require.NoError(t, err) require.Equal(t, uint64(2), idx) require.Equal(t, updated, config) } func TestStore_ConfigEntry_DeleteCAS(t *testing.T) { s := testConfigStateStore(t) entry := &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: "global", Config: map[string]interface{}{ "DestinationServiceName": "foo", }, } // Attempt to delete the entry before it exists. ok, err := s.DeleteConfigEntryCAS(1, 0, entry) require.NoError(t, err) require.False(t, ok) // Create the entry. require.NoError(t, s.EnsureConfigEntry(1, entry)) // Attempt to delete with an invalid index. ok, err = s.DeleteConfigEntryCAS(2, 99, entry) require.NoError(t, err) require.False(t, ok) // Entry should not be deleted. _, config, err := s.ConfigEntry(nil, entry.Kind, entry.Name, nil) require.NoError(t, err) require.NotNil(t, config) // Attempt to delete with a valid index. ok, err = s.DeleteConfigEntryCAS(2, 1, entry) require.NoError(t, err) require.True(t, ok) // Entry should be deleted. _, config, err = s.ConfigEntry(nil, entry.Kind, entry.Name, nil) require.NoError(t, err) require.Nil(t, 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) { 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(t, s.EnsureConfigEntry(0, entry1)) require.NoError(t, s.EnsureConfigEntry(1, entry2)) require.NoError(t, s.EnsureConfigEntry(2, entry3)) // Get all entries idx, entries, err := s.ConfigEntries(nil, nil) require.NoError(t, err) require.Equal(t, uint64(2), idx) require.Equal(t, []structs.ConfigEntry{entry1, entry2, entry3}, entries) // Get all proxy entries idx, entries, err = s.ConfigEntriesByKind(nil, structs.ProxyDefaults, nil) require.NoError(t, err) require.Equal(t, uint64(2), idx) require.Equal(t, []structs.ConfigEntry{entry1}, entries) // Get all service entries ws := memdb.NewWatchSet() idx, entries, err = s.ConfigEntriesByKind(ws, structs.ServiceDefaults, nil) require.NoError(t, err) require.Equal(t, uint64(2), idx) require.Equal(t, []structs.ConfigEntry{entry2, entry3}, entries) // Watch should not have fired require.False(t, watchFired(ws)) // Now make an update and make sure the watch fires. require.NoError(t, s.EnsureConfigEntry(3, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "test2", Protocol: "tcp", })) require.True(t, watchFired(ws)) } func TestStore_ServiceDefaults_Kind_Destination(t *testing.T) { s := testConfigStateStore(t) Gtwy := &structs.TerminatingGatewayConfigEntry{ Kind: structs.TerminatingGateway, Name: "Gtwy1", Services: []structs.LinkedService{ { Name: "dest1", }, }, } // Create require.NoError(t, s.EnsureConfigEntry(0, Gtwy)) destination := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "dest1", Destination: &structs.DestinationConfig{}, } _, gatewayServices, err := s.GatewayServices(nil, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindUnknown) ws := memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) // Create require.NoError(t, s.EnsureConfigEntry(0, destination)) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindDestination) _, kindServices, err := s.ServiceNamesOfKind(ws, structs.ServiceKindDestination) require.NoError(t, err) require.Len(t, kindServices, 1) require.Equal(t, kindServices[0].Kind, structs.ServiceKindDestination) ws = memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.NoError(t, s.DeleteConfigEntry(6, structs.ServiceDefaults, destination.Name, &destination.EnterpriseMeta)) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, structs.GatewayServiceKindUnknown, gatewayServices[0].ServiceKind) _, kindServices, err = s.ServiceNamesOfKind(ws, structs.ServiceKindDestination) require.NoError(t, err) require.Len(t, kindServices, 0) } func TestStore_ServiceDefaults_Kind_NotDestination(t *testing.T) { s := testConfigStateStore(t) Gtwy := &structs.TerminatingGatewayConfigEntry{ Kind: structs.TerminatingGateway, Name: "Gtwy1", Services: []structs.LinkedService{ { Name: "dest1", }, }, } // Create require.NoError(t, s.EnsureConfigEntry(0, Gtwy)) destination := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "dest1", } _, gatewayServices, err := s.GatewayServices(nil, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindUnknown) ws := memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) // Create require.NoError(t, s.EnsureConfigEntry(0, destination)) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.False(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindUnknown) ws = memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.NoError(t, s.DeleteConfigEntry(6, structs.ServiceDefaults, destination.Name, &destination.EnterpriseMeta)) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.False(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindUnknown) } func TestStore_Service_TerminatingGateway_Kind_Service_Destination(t *testing.T) { s := testConfigStateStore(t) Gtwy := &structs.TerminatingGatewayConfigEntry{ Kind: structs.TerminatingGateway, Name: "Gtwy1", Services: []structs.LinkedService{ { Name: "web", }, }, } // Create require.NoError(t, s.EnsureConfigEntry(0, Gtwy)) service := &structs.NodeService{ Kind: structs.ServiceKindTypical, Service: "web", } destination := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Destination: &structs.DestinationConfig{}, } _, gatewayServices, err := s.GatewayServices(nil, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindUnknown) ws := memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) // Create require.NoError(t, s.EnsureNode(0, &structs.Node{Node: "node1"})) require.NoError(t, s.EnsureService(0, "node1", service)) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindService) _, kindServices, err := s.ServiceNamesOfKind(ws, structs.ServiceKindTypical) require.NoError(t, err) require.Len(t, kindServices, 1) require.Equal(t, kindServices[0].Kind, structs.ServiceKindTypical) require.NoError(t, s.EnsureConfigEntry(0, destination)) _, gatewayServices, err = s.GatewayServices(nil, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindService) _, kindServices, err = s.ServiceNamesOfKind(ws, structs.ServiceKindTypical) require.NoError(t, err) require.Len(t, kindServices, 1) require.Equal(t, kindServices[0].Kind, structs.ServiceKindTypical) ws = memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.NoError(t, s.DeleteService(6, "node1", service.ID, &service.EnterpriseMeta, "")) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindDestination) _, kindServices, err = s.ServiceNamesOfKind(ws, structs.ServiceKindDestination) require.NoError(t, err) require.Len(t, kindServices, 1) require.Equal(t, kindServices[0].Kind, structs.ServiceKindDestination) } func TestStore_Service_TerminatingGateway_Kind_Destination_Service(t *testing.T) { s := testConfigStateStore(t) Gtwy := &structs.TerminatingGatewayConfigEntry{ Kind: structs.TerminatingGateway, Name: "Gtwy1", Services: []structs.LinkedService{ { Name: "web", }, }, } // Create require.NoError(t, s.EnsureConfigEntry(0, Gtwy)) service := &structs.NodeService{ Kind: structs.ServiceKindTypical, Service: "web", } destination := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Destination: &structs.DestinationConfig{}, } _, gatewayServices, err := s.GatewayServices(nil, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindUnknown) ws := memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) // Create require.NoError(t, s.EnsureConfigEntry(0, destination)) _, gatewayServices, err = s.GatewayServices(nil, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindDestination) _, kindServices, err := s.ServiceNamesOfKind(ws, structs.ServiceKindDestination) require.NoError(t, err) require.Len(t, kindServices, 1) require.Equal(t, kindServices[0].Kind, structs.ServiceKindDestination) require.NoError(t, s.EnsureNode(0, &structs.Node{Node: "node1"})) require.NoError(t, s.EnsureService(0, "node1", service)) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindService) _, kindServices, err = s.ServiceNamesOfKind(ws, structs.ServiceKindTypical) require.NoError(t, err) require.Len(t, kindServices, 1) require.Equal(t, kindServices[0].Kind, structs.ServiceKindTypical) ws = memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.NoError(t, s.DeleteService(6, "node1", service.ID, &service.EnterpriseMeta, "")) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindDestination) _, kindServices, err = s.ServiceNamesOfKind(ws, structs.ServiceKindDestination) require.NoError(t, err) require.Len(t, kindServices, 1) require.Equal(t, kindServices[0].Kind, structs.ServiceKindDestination) } func TestStore_Service_TerminatingGateway_Kind_Service(t *testing.T) { s := testConfigStateStore(t) Gtwy := &structs.TerminatingGatewayConfigEntry{ Kind: structs.TerminatingGateway, Name: "Gtwy1", Services: []structs.LinkedService{ { Name: "web", }, }, } // Create require.NoError(t, s.EnsureConfigEntry(0, Gtwy)) service := &structs.NodeService{ Kind: structs.ServiceKindTypical, Service: "web", } _, gatewayServices, err := s.GatewayServices(nil, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindUnknown) ws := memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) // Create require.NoError(t, s.EnsureNode(0, &structs.Node{Node: "node1"})) require.NoError(t, s.EnsureService(0, "node1", service)) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindService) ws = memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.NoError(t, s.DeleteService(6, "node1", service.ID, &service.EnterpriseMeta, "")) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindUnknown) } func TestStore_ServiceDefaults_Kind_Destination_Wildcard(t *testing.T) { s := testConfigStateStore(t) Gtwy := &structs.TerminatingGatewayConfigEntry{ Kind: structs.TerminatingGateway, Name: "Gtwy1", Services: []structs.LinkedService{ { Name: "*", }, }, } // Create require.NoError(t, s.EnsureConfigEntry(0, Gtwy)) destination := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "dest1", Destination: &structs.DestinationConfig{}, } _, gatewayServices, err := s.GatewayServices(nil, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 0) ws := memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) // Create require.NoError(t, s.EnsureConfigEntry(0, destination)) require.NoError(t, err) require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindDestination) ws = memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.NoError(t, s.DeleteConfigEntry(6, structs.ServiceDefaults, destination.Name, &destination.EnterpriseMeta)) // Watch is fired because we deleted the destination - now the mapping should be gone. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 0) t.Run("delete service instance before config entry", func(t *testing.T) { // Set up a service with both a real instance and destination from a config entry. require.NoError(t, s.EnsureNode(7, &structs.Node{Node: "foo", Address: "127.0.0.1"})) require.NoError(t, s.EnsureService(8, "foo", &structs.NodeService{ID: "dest2", Service: "dest2", Tags: nil, Address: "", Port: 5000})) ws = memdb.NewWatchSet() _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, structs.GatewayServiceKindService, gatewayServices[0].ServiceKind) // Register destination; shouldn't change the gateway mapping. destination2 := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "dest2", Destination: &structs.DestinationConfig{}, } require.NoError(t, s.EnsureConfigEntry(9, destination2)) require.False(t, watchFired(ws)) ws = memdb.NewWatchSet() _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) expected := structs.GatewayServices{ { Service: structs.NewServiceName("dest2", nil), Gateway: structs.NewServiceName("Gtwy1", nil), ServiceKind: structs.GatewayServiceKindService, GatewayKind: structs.ServiceKindTerminatingGateway, FromWildcard: true, RaftIndex: structs.RaftIndex{ CreateIndex: 8, ModifyIndex: 8, }, }, } require.Equal(t, expected, gatewayServices) // Delete the service, mapping should still exist. require.NoError(t, s.DeleteService(10, "foo", "dest2", nil, "")) require.False(t, watchFired(ws)) ws = memdb.NewWatchSet() _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, expected, gatewayServices) // Delete the config entry, mapping should be gone. require.NoError(t, s.DeleteConfigEntry(11, structs.ServiceDefaults, "dest2", &destination.EnterpriseMeta)) require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Empty(t, gatewayServices) }) t.Run("delete config entry before service instance", func(t *testing.T) { // Set up a service with both a real instance and destination from a config entry. destination2 := &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "dest2", Destination: &structs.DestinationConfig{}, } require.NoError(t, s.EnsureConfigEntry(7, destination2)) ws = memdb.NewWatchSet() _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) expected := structs.GatewayServices{ { Service: structs.NewServiceName("dest2", nil), Gateway: structs.NewServiceName("Gtwy1", nil), ServiceKind: structs.GatewayServiceKindDestination, GatewayKind: structs.ServiceKindTerminatingGateway, FromWildcard: true, RaftIndex: structs.RaftIndex{ CreateIndex: 7, ModifyIndex: 7, }, }, } require.Equal(t, expected, gatewayServices) // Register service, only ServiceKind should have changed on the gateway mapping. require.NoError(t, s.EnsureNode(8, &structs.Node{Node: "foo", Address: "127.0.0.1"})) require.NoError(t, s.EnsureService(9, "foo", &structs.NodeService{ID: "dest2", Service: "dest2", Tags: nil, Address: "", Port: 5000})) require.True(t, watchFired(ws)) ws = memdb.NewWatchSet() _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) expected = structs.GatewayServices{ { Service: structs.NewServiceName("dest2", nil), Gateway: structs.NewServiceName("Gtwy1", nil), ServiceKind: structs.GatewayServiceKindService, GatewayKind: structs.ServiceKindTerminatingGateway, FromWildcard: true, RaftIndex: structs.RaftIndex{ CreateIndex: 7, ModifyIndex: 9, }, }, } require.Equal(t, expected, gatewayServices) // Delete the config entry, mapping should still exist. require.NoError(t, s.DeleteConfigEntry(10, structs.ServiceDefaults, "dest2", &destination.EnterpriseMeta)) require.False(t, watchFired(ws)) ws = memdb.NewWatchSet() _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, expected, gatewayServices) // Delete the service, mapping should be gone. require.NoError(t, s.DeleteService(11, "foo", "dest2", nil, "")) require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Empty(t, gatewayServices) }) } func TestStore_Service_TerminatingGateway_Kind_Service_Wildcard(t *testing.T) { s := testConfigStateStore(t) Gtwy := &structs.TerminatingGatewayConfigEntry{ Kind: structs.TerminatingGateway, Name: "Gtwy1", Services: []structs.LinkedService{ { Name: "*", }, }, } // Create require.NoError(t, s.EnsureConfigEntry(0, Gtwy)) service := &structs.NodeService{ Kind: structs.ServiceKindTypical, Service: "web", } _, gatewayServices, err := s.GatewayServices(nil, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 0) ws := memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) // Create require.NoError(t, s.EnsureNode(0, &structs.Node{Node: "node1"})) require.NoError(t, s.EnsureService(0, "node1", service)) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 1) require.Equal(t, gatewayServices[0].ServiceKind, structs.GatewayServiceKindService) ws = memdb.NewWatchSet() _, _, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.NoError(t, s.DeleteService(6, "node1", service.ID, &service.EnterpriseMeta, "")) //Watch is fired because we transitioned to a destination, by default we assume it's not. require.True(t, watchFired(ws)) _, gatewayServices, err = s.GatewayServices(ws, "Gtwy1", nil) require.NoError(t, err) require.Len(t, gatewayServices, 0) } func TestStore_ConfigEntry_GraphValidation(t *testing.T) { ensureConfigEntry := func(s *Store, idx uint64, entry structs.ConfigEntry) error { if err := entry.Normalize(); err != nil { return err } if err := entry.Validate(); err != nil { return err } return s.EnsureConfigEntry(idx, entry) } type tcase struct { entries []structs.ConfigEntry opAdd structs.ConfigEntry opDelete configentry.KindName expectErr string expectGraphErr bool } EMPTY_KN := configentry.KindName{} run := func(t *testing.T, tc tcase) { s := testConfigStateStore(t) for _, entry := range tc.entries { require.NoError(t, ensureConfigEntry(s, 0, entry)) } nOps := 0 if tc.opAdd != nil { nOps++ } if tc.opDelete != EMPTY_KN { nOps++ } require.Equal(t, 1, nOps, "exactly one operation is required") var err error switch { case tc.opAdd != nil: err = ensureConfigEntry(s, 0, tc.opAdd) case tc.opDelete != EMPTY_KN: kn := tc.opDelete err = s.DeleteConfigEntry(0, kn.Kind, kn.Name, &kn.EnterpriseMeta) default: t.Fatal("not possible") } 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) } } cases := map[string]tcase{ "splitter fails without default protocol": { entries: []structs.ConfigEntry{}, opAdd: &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, 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", }, }, opAdd: &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100}, }, }, 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.DefaultEnterpriseMetaInDefaultPartition(), }, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Subsets: map[string]structs.ServiceResolverSubset{ "v1": { Filter: "Service.Meta.version == v1", }, "v2": { Filter: "Service.Meta.version == v2", }, }, }, }, opAdd: &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90, ServiceSubset: "v1"}, {Weight: 10, ServiceSubset: "v2"}, }, EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), }, }, "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", }, }, }, }, opAdd: &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90, ServiceSubset: "v1"}, {Weight: 10, ServiceSubset: "v2"}, }, }, }, "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", }, }, }, }, opAdd: &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/other", }, }, Destination: &structs.ServiceRouteDestination{ ServiceSubset: "other", }, }, }, }, 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", }, }, }, }, opAdd: &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/other", }, }, Destination: &structs.ServiceRouteDestination{ ServiceSubset: "other", }, }, }, }, 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"}, }, }, }, opDelete: configentry.NewKindName(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"}, }, }, }, opDelete: configentry.NewKindName(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"}, }, }, }, opDelete: configentry.NewKindName(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"}, }, }, }, opAdd: &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", }, 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", }, }, }, }, }, opDelete: configentry.NewKindName(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", }, }, }, }, }, opAdd: &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", }, 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", }, }, opAdd: &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 90}, {Weight: 10, Service: "other"}, }, }, 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", }, }, opAdd: &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/other", }, }, Destination: &structs.ServiceRouteDestination{ Service: "other", }, }, }, }, 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, }, }, opAdd: &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": { Service: "other", }, }, }, 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, }, }, opAdd: &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, }, 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", }, }, }, }, opAdd: &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", ServiceSubset: "v1", }, }, }, "cannot redirect to a subset that does not exist": { entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "other", ConnectTimeout: 33 * time.Second, }, }, opAdd: &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", ServiceSubset: "v1", }, }, 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", }, }, }, opAdd: &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, }, 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"}, }, }, }, opAdd: &structs.ServiceSplitterConfigEntry{ Kind: "service-splitter", Name: "main", Splits: []structs.ServiceSplit{ {Weight: 100, Service: "other"}, }, }, expectErr: `detected circular reference`, expectGraphErr: true, }, ///////////////////////////////////////////////// "cannot peer export cross-dc redirect": { entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Datacenter: "dc3", }, }, }, opAdd: &structs.ExportedServicesConfigEntry{ Name: "default", Services: []structs.ExportedService{{ Name: "main", Consumers: []structs.ServiceConsumer{{Peer: "my-peer"}}, }}, }, expectErr: `contains cross-datacenter resolver redirect`, }, "cannot peer export cross-dc redirect via wildcard": { entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Redirect: &structs.ServiceResolverRedirect{ Datacenter: "dc3", }, }, }, opAdd: &structs.ExportedServicesConfigEntry{ Name: "default", Services: []structs.ExportedService{{ Name: "*", Consumers: []structs.ServiceConsumer{{Peer: "my-peer"}}, }}, }, expectErr: `contains cross-datacenter resolver redirect`, }, "cannot peer export cross-dc failover": { entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": { Datacenters: []string{"dc3"}, }, }, }, }, opAdd: &structs.ExportedServicesConfigEntry{ Name: "default", Services: []structs.ExportedService{{ Name: "main", Consumers: []structs.ServiceConsumer{{Peer: "my-peer"}}, }}, }, expectErr: `contains cross-datacenter failover`, }, "cannot peer export cross-dc failover via wildcard": { entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: "service-resolver", Name: "main", Failover: map[string]structs.ServiceResolverFailover{ "*": { Datacenters: []string{"dc3"}, }, }, }, }, opAdd: &structs.ExportedServicesConfigEntry{ Name: "default", Services: []structs.ExportedService{{ Name: "*", Consumers: []structs.ServiceConsumer{{Peer: "my-peer"}}, }}, }, expectErr: `contains cross-datacenter failover`, }, "cannot redirect a peer exported tcp service": { entries: []structs.ConfigEntry{ &structs.ExportedServicesConfigEntry{ Name: "default", Services: []structs.ExportedService{{ Name: "main", Consumers: []structs.ServiceConsumer{{Peer: "my-peer"}}, }}, }, }, opAdd: &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", Redirect: &structs.ServiceResolverRedirect{ Service: "other", }, }, expectErr: `cannot introduce new discovery chain targets like`, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { run(t, tc) }) } } func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) { for _, tc := range []struct { name string entries []structs.ConfigEntry expectBefore []configentry.KindName overrides map[configentry.KindName]structs.ConfigEntry expectAfter []configentry.KindName expectAfterErr string checkAfter func(t *testing.T, entrySet *configentry.DiscoveryChainSet) }{ { name: "mask service-defaults", entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", }, }, expectBefore: []configentry.KindName{ configentry.NewKindName(structs.ServiceDefaults, "main", nil), }, overrides: map[configentry.KindName]structs.ConfigEntry{ configentry.NewKindName(structs.ServiceDefaults, "main", nil): nil, }, expectAfter: []configentry.KindName{ // nothing }, }, { name: "edit service-defaults", entries: []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "tcp", }, }, expectBefore: []configentry.KindName{ configentry.NewKindName(structs.ServiceDefaults, "main", nil), }, overrides: map[configentry.KindName]structs.ConfigEntry{ configentry.NewKindName(structs.ServiceDefaults, "main", nil): &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "main", Protocol: "grpc", }, }, expectAfter: []configentry.KindName{ configentry.NewKindName(structs.ServiceDefaults, "main", nil), }, checkAfter: func(t *testing.T, entrySet *configentry.DiscoveryChainSet) { 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: []configentry.KindName{ configentry.NewKindName(structs.ServiceDefaults, "main", nil), configentry.NewKindName(structs.ServiceRouter, "main", nil), }, overrides: map[configentry.KindName]structs.ConfigEntry{ configentry.NewKindName(structs.ServiceRouter, "main", nil): nil, }, expectAfter: []configentry.KindName{ configentry.NewKindName(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: []configentry.KindName{ configentry.NewKindName(structs.ServiceDefaults, "main", nil), configentry.NewKindName(structs.ServiceResolver, "main", nil), configentry.NewKindName(structs.ServiceRouter, "main", nil), }, overrides: map[configentry.KindName]structs.ConfigEntry{ configentry.NewKindName(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: []configentry.KindName{ configentry.NewKindName(structs.ServiceDefaults, "main", nil), configentry.NewKindName(structs.ServiceResolver, "main", nil), configentry.NewKindName(structs.ServiceRouter, "main", nil), }, checkAfter: func(t *testing.T, entrySet *configentry.DiscoveryChainSet) { 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: []configentry.KindName{ configentry.NewKindName(structs.ServiceDefaults, "main", nil), configentry.NewKindName(structs.ServiceSplitter, "main", nil), }, overrides: map[configentry.KindName]structs.ConfigEntry{ configentry.NewKindName(structs.ServiceSplitter, "main", nil): nil, }, expectAfter: []configentry.KindName{ configentry.NewKindName(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: []configentry.KindName{ configentry.NewKindName(structs.ServiceDefaults, "main", nil), configentry.NewKindName(structs.ServiceSplitter, "main", nil), }, overrides: map[configentry.KindName]structs.ConfigEntry{ configentry.NewKindName(structs.ServiceSplitter, "main", nil): &structs.ServiceSplitterConfigEntry{ Kind: structs.ServiceSplitter, Name: "main", Splits: []structs.ServiceSplit{ {Weight: 85, ServiceSubset: "v1"}, {Weight: 15, ServiceSubset: "v2"}, }, }, }, expectAfter: []configentry.KindName{ configentry.NewKindName(structs.ServiceDefaults, "main", nil), configentry.NewKindName(structs.ServiceSplitter, "main", nil), }, checkAfter: func(t *testing.T, entrySet *configentry.DiscoveryChainSet) { 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: []configentry.KindName{ configentry.NewKindName(structs.ServiceResolver, "main", nil), }, overrides: map[configentry.KindName]structs.ConfigEntry{ configentry.NewKindName(structs.ServiceResolver, "main", nil): nil, }, expectAfter: []configentry.KindName{ // nothing }, }, { name: "edit service-resolver", entries: []structs.ConfigEntry{ &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", }, }, expectBefore: []configentry.KindName{ configentry.NewKindName(structs.ServiceResolver, "main", nil), }, overrides: map[configentry.KindName]structs.ConfigEntry{ configentry.NewKindName(structs.ServiceResolver, "main", nil): &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "main", ConnectTimeout: 33 * time.Second, }, }, expectAfter: []configentry.KindName{ configentry.NewKindName(structs.ServiceResolver, "main", nil), }, checkAfter: func(t *testing.T, entrySet *configentry.DiscoveryChainSet) { 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 *configentry.DiscoveryChainSet) []configentry.KindName { var out []configentry.KindName for _, entry := range entrySet.Routers { out = append(out, configentry.NewKindName( entry.Kind, entry.Name, &entry.EnterpriseMeta, )) } for _, entry := range entrySet.Splitters { out = append(out, configentry.NewKindName( entry.Kind, entry.Name, &entry.EnterpriseMeta, )) } for _, entry := range entrySet.Resolvers { out = append(out, configentry.NewKindName( entry.Kind, entry.Name, &entry.EnterpriseMeta, )) } for _, entry := range entrySet.Services { out = append(out, configentry.NewKindName( entry.Kind, entry.Name, &entry.EnterpriseMeta, )) } for _, entry := range entrySet.ProxyDefaults { out = append(out, configentry.NewKindName( 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.DefaultEnterpriseMetaInDefaultPartition() 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.DefaultEnterpriseMetaInDefaultPartition()) 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.DefaultEnterpriseMetaInDefaultPartition() 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) } } }) } }