diff --git a/pkg/api/types_service.go b/pkg/api/types_service.go index 2de0b28..11f9533 100644 --- a/pkg/api/types_service.go +++ b/pkg/api/types_service.go @@ -2,9 +2,12 @@ package api // Service represents a published service in API7 EE (runtime). type Service struct { - ID string `json:"id,omitempty" yaml:"id,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` + // Type marks an L4 service ("stream"); absent/"http" for L7. Stream routes + // can only bind to stream-typed services, so it must survive export. + Type string `json:"type,omitempty" yaml:"type,omitempty"` Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` Upstream map[string]interface{} `json:"upstream,omitempty" yaml:"upstream,omitempty"` UpstreamID string `json:"upstream_id,omitempty" yaml:"upstream_id,omitempty"` diff --git a/pkg/cmd/config/dump/dump_test.go b/pkg/cmd/config/dump/dump_test.go index ce57c10..1715abb 100644 --- a/pkg/cmd/config/dump/dump_test.go +++ b/pkg/cmd/config/dump/dump_test.go @@ -250,3 +250,41 @@ func TestConfigDump_StreamRoutesDisabled(t *testing.T) { assert.NotContains(t, result, "stream_routes") reg.Verify(t) } + +// TestConfigDump_L4ServiceKeepsTypeAndPlugins guards against the export +// dropping L4 (stream) service configuration. A stream service is marked by +// `type: stream`; without it the service re-imports as L7 and its stream +// routes can no longer bind. Plugins must survive too. +func TestConfigDump_L4ServiceKeepsTypeAndPlugins(t *testing.T) { + reg := &httpmock.Registry{} + registerEmptyResources(reg, map[string]bool{"/apisix/admin/services": true}) + reg.Register(http.MethodGet, "/apisix/admin/services", httpmock.JSONResponse(`{ + "total": 1, + "list": [{ + "id": "l4-svc", + "name": "tcp-svc", + "type": "stream", + "plugins": {"limit-conn": {"conn": 10, "burst": 5, "default_conn_delay": 0.1, "key": "remote_addr"}}, + "upstream": {"type": "roundrobin", "nodes": [{"host": "10.0.0.1", "port": 6379, "weight": 1}]} + }] + }`)) + // Routes are fetched per-service; return none for the stream service. + reg.Register(http.MethodGet, "/apisix/admin/routes", httpmock.JSONResponse(`{"total":0,"list":[]}`)) + + ios, _, stdout, _ := iostreams.Test() + + c := NewCmdDump(newFactory(reg, ios)) + c.SetArgs([]string{"--output", "json"}) + require.NoError(t, c.Execute()) + + var result map[string]interface{} + require.NoError(t, json.Unmarshal(stdout.Bytes(), &result)) + + services := result["services"].([]interface{}) + require.Len(t, services, 1) + svc := services[0].(map[string]interface{}) + assert.Equal(t, "stream", svc["type"], "L4 service type must survive export") + plugins := svc["plugins"].(map[string]interface{}) + assert.Contains(t, plugins, "limit-conn", "L4 service plugins must survive export") + reg.Verify(t) +}