Creates HTTP endpoint registry.

This commit is contained in:
James Phillips 2017-11-28 16:06:26 -08:00
parent 679775418f
commit 68f100c8df
No known key found for this signature in database
GPG Key ID: 77183E682AC5FC11
4 changed files with 215 additions and 182 deletions

View File

@ -39,6 +39,29 @@ type HTTPServer struct {
proto string
}
// endpoint is a Consul-specific HTTP handler that takes the usual arguments in
// but returns a response object and error, both of which are handled in a
// common manner by Consul's HTTP server.
type endpoint func(resp http.ResponseWriter, req *http.Request) (interface{}, error)
// unboundEndpoint is an endpoint method on a server.
type unboundEndpoint func(s *HTTPServer, resp http.ResponseWriter, req *http.Request) (interface{}, error)
// endpoints is a map from URL pattern to unbound endpoint.
var endpoints map[string]unboundEndpoint
// registerEndpoint registers a new endpoint, which should be done at package
// init() time.
func registerEndpoint(pattern string, fn unboundEndpoint) {
if endpoints == nil {
endpoints = make(map[string]unboundEndpoint)
}
if endpoints[pattern] != nil {
panic(fmt.Errorf("Pattern %q is already registered", pattern))
}
endpoints[pattern] = fn
}
// handler is used to attach our handlers to the mux
func (s *HTTPServer) handler(enableDebug bool) http.Handler {
mux := http.NewServeMux()
@ -75,77 +98,13 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler {
}
mux.HandleFunc("/", s.Index)
// API V1.
handleFuncMetrics("/v1/acl/bootstrap", s.wrap(s.ACLBootstrap))
handleFuncMetrics("/v1/acl/create", s.wrap(s.ACLCreate))
handleFuncMetrics("/v1/acl/update", s.wrap(s.ACLUpdate))
handleFuncMetrics("/v1/acl/destroy/", s.wrap(s.ACLDestroy))
handleFuncMetrics("/v1/acl/info/", s.wrap(s.ACLGet))
handleFuncMetrics("/v1/acl/clone/", s.wrap(s.ACLClone))
handleFuncMetrics("/v1/acl/list", s.wrap(s.ACLList))
handleFuncMetrics("/v1/acl/replication", s.wrap(s.ACLReplicationStatus))
handleFuncMetrics("/v1/agent/token/", s.wrap(s.AgentToken))
handleFuncMetrics("/v1/agent/self", s.wrap(s.AgentSelf))
handleFuncMetrics("/v1/agent/maintenance", s.wrap(s.AgentNodeMaintenance))
handleFuncMetrics("/v1/agent/reload", s.wrap(s.AgentReload))
handleFuncMetrics("/v1/agent/monitor", s.wrap(s.AgentMonitor))
handleFuncMetrics("/v1/agent/metrics", s.wrap(s.AgentMetrics))
handleFuncMetrics("/v1/agent/services", s.wrap(s.AgentServices))
handleFuncMetrics("/v1/agent/checks", s.wrap(s.AgentChecks))
handleFuncMetrics("/v1/agent/members", s.wrap(s.AgentMembers))
handleFuncMetrics("/v1/agent/join/", s.wrap(s.AgentJoin))
handleFuncMetrics("/v1/agent/leave", s.wrap(s.AgentLeave))
handleFuncMetrics("/v1/agent/force-leave/", s.wrap(s.AgentForceLeave))
handleFuncMetrics("/v1/agent/check/register", s.wrap(s.AgentRegisterCheck))
handleFuncMetrics("/v1/agent/check/deregister/", s.wrap(s.AgentDeregisterCheck))
handleFuncMetrics("/v1/agent/check/pass/", s.wrap(s.AgentCheckPass))
handleFuncMetrics("/v1/agent/check/warn/", s.wrap(s.AgentCheckWarn))
handleFuncMetrics("/v1/agent/check/fail/", s.wrap(s.AgentCheckFail))
handleFuncMetrics("/v1/agent/check/update/", s.wrap(s.AgentCheckUpdate))
handleFuncMetrics("/v1/agent/service/register", s.wrap(s.AgentRegisterService))
handleFuncMetrics("/v1/agent/service/deregister/", s.wrap(s.AgentDeregisterService))
handleFuncMetrics("/v1/agent/service/maintenance/", s.wrap(s.AgentServiceMaintenance))
handleFuncMetrics("/v1/catalog/register", s.wrap(s.CatalogRegister))
handleFuncMetrics("/v1/catalog/deregister", s.wrap(s.CatalogDeregister))
handleFuncMetrics("/v1/catalog/datacenters", s.wrap(s.CatalogDatacenters))
handleFuncMetrics("/v1/catalog/nodes", s.wrap(s.CatalogNodes))
handleFuncMetrics("/v1/catalog/services", s.wrap(s.CatalogServices))
handleFuncMetrics("/v1/catalog/service/", s.wrap(s.CatalogServiceNodes))
handleFuncMetrics("/v1/catalog/node/", s.wrap(s.CatalogNodeServices))
handleFuncMetrics("/v1/coordinate/datacenters", s.wrap(s.CoordinateDatacenters))
handleFuncMetrics("/v1/coordinate/nodes", s.wrap(s.CoordinateNodes))
handleFuncMetrics("/v1/coordinate/node/", s.wrap(s.CoordinateNode))
handleFuncMetrics("/v1/coordinate/update", s.wrap(s.CoordinateUpdate))
handleFuncMetrics("/v1/event/fire/", s.wrap(s.EventFire))
handleFuncMetrics("/v1/event/list", s.wrap(s.EventList))
handleFuncMetrics("/v1/health/node/", s.wrap(s.HealthNodeChecks))
handleFuncMetrics("/v1/health/checks/", s.wrap(s.HealthServiceChecks))
handleFuncMetrics("/v1/health/state/", s.wrap(s.HealthChecksInState))
handleFuncMetrics("/v1/health/service/", s.wrap(s.HealthServiceNodes))
handleFuncMetrics("/v1/internal/ui/nodes", s.wrap(s.UINodes))
handleFuncMetrics("/v1/internal/ui/node/", s.wrap(s.UINodeInfo))
handleFuncMetrics("/v1/internal/ui/services", s.wrap(s.UIServices))
handleFuncMetrics("/v1/kv/", s.wrap(s.KVSEndpoint))
handleFuncMetrics("/v1/operator/raft/configuration", s.wrap(s.OperatorRaftConfiguration))
handleFuncMetrics("/v1/operator/raft/peer", s.wrap(s.OperatorRaftPeer))
handleFuncMetrics("/v1/operator/keyring", s.wrap(s.OperatorKeyringEndpoint))
handleFuncMetrics("/v1/operator/autopilot/configuration", s.wrap(s.OperatorAutopilotConfiguration))
handleFuncMetrics("/v1/operator/autopilot/health", s.wrap(s.OperatorServerHealth))
handleFuncMetrics("/v1/query", s.wrap(s.PreparedQueryGeneral))
handleFuncMetrics("/v1/query/", s.wrap(s.PreparedQuerySpecific))
handleFuncMetrics("/v1/session/create", s.wrap(s.SessionCreate))
handleFuncMetrics("/v1/session/destroy/", s.wrap(s.SessionDestroy))
handleFuncMetrics("/v1/session/renew/", s.wrap(s.SessionRenew))
handleFuncMetrics("/v1/session/info/", s.wrap(s.SessionGet))
handleFuncMetrics("/v1/session/node/", s.wrap(s.SessionsForNode))
handleFuncMetrics("/v1/session/list", s.wrap(s.SessionList))
handleFuncMetrics("/v1/status/leader", s.wrap(s.StatusLeader))
handleFuncMetrics("/v1/status/peers", s.wrap(s.StatusPeers))
handleFuncMetrics("/v1/snapshot", s.wrap(s.Snapshot))
handleFuncMetrics("/v1/txn", s.wrap(s.Txn))
// Debug endpoints.
for pattern, fn := range endpoints {
thisFn := fn
bound := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
return thisFn(s, resp, req)
}
handleFuncMetrics(pattern, s.wrap(bound))
}
if enableDebug {
handleFuncMetrics("/debug/pprof/", pprof.Index)
handleFuncMetrics("/debug/pprof/cmdline", pprof.Cmdline)
@ -186,7 +145,7 @@ var (
)
// wrap is used to wrap functions to make them more convenient
func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) http.HandlerFunc {
func (s *HTTPServer) wrap(handler endpoint) http.HandlerFunc {
return func(resp http.ResponseWriter, req *http.Request) {
setHeaders(resp, s.agent.config.HTTPResponseHeaders)
setTranslateAddr(resp, s.agent.config.TranslateWANAddrs)

71
agent/http_oss.go Normal file
View File

@ -0,0 +1,71 @@
package agent
func init() {
registerEndpoint("/v1/acl/bootstrap", (*HTTPServer).ACLBootstrap)
registerEndpoint("/v1/acl/create", (*HTTPServer).ACLCreate)
registerEndpoint("/v1/acl/update", (*HTTPServer).ACLUpdate)
registerEndpoint("/v1/acl/destroy/", (*HTTPServer).ACLDestroy)
registerEndpoint("/v1/acl/info/", (*HTTPServer).ACLGet)
registerEndpoint("/v1/acl/clone/", (*HTTPServer).ACLClone)
registerEndpoint("/v1/acl/list", (*HTTPServer).ACLList)
registerEndpoint("/v1/acl/replication", (*HTTPServer).ACLReplicationStatus)
registerEndpoint("/v1/agent/token/", (*HTTPServer).AgentToken)
registerEndpoint("/v1/agent/self", (*HTTPServer).AgentSelf)
registerEndpoint("/v1/agent/maintenance", (*HTTPServer).AgentNodeMaintenance)
registerEndpoint("/v1/agent/reload", (*HTTPServer).AgentReload)
registerEndpoint("/v1/agent/monitor", (*HTTPServer).AgentMonitor)
registerEndpoint("/v1/agent/metrics", (*HTTPServer).AgentMetrics)
registerEndpoint("/v1/agent/services", (*HTTPServer).AgentServices)
registerEndpoint("/v1/agent/checks", (*HTTPServer).AgentChecks)
registerEndpoint("/v1/agent/members", (*HTTPServer).AgentMembers)
registerEndpoint("/v1/agent/join/", (*HTTPServer).AgentJoin)
registerEndpoint("/v1/agent/leave", (*HTTPServer).AgentLeave)
registerEndpoint("/v1/agent/force-leave/", (*HTTPServer).AgentForceLeave)
registerEndpoint("/v1/agent/check/register", (*HTTPServer).AgentRegisterCheck)
registerEndpoint("/v1/agent/check/deregister/", (*HTTPServer).AgentDeregisterCheck)
registerEndpoint("/v1/agent/check/pass/", (*HTTPServer).AgentCheckPass)
registerEndpoint("/v1/agent/check/warn/", (*HTTPServer).AgentCheckWarn)
registerEndpoint("/v1/agent/check/fail/", (*HTTPServer).AgentCheckFail)
registerEndpoint("/v1/agent/check/update/", (*HTTPServer).AgentCheckUpdate)
registerEndpoint("/v1/agent/service/register", (*HTTPServer).AgentRegisterService)
registerEndpoint("/v1/agent/service/deregister/", (*HTTPServer).AgentDeregisterService)
registerEndpoint("/v1/agent/service/maintenance/", (*HTTPServer).AgentServiceMaintenance)
registerEndpoint("/v1/catalog/register", (*HTTPServer).CatalogRegister)
registerEndpoint("/v1/catalog/deregister", (*HTTPServer).CatalogDeregister)
registerEndpoint("/v1/catalog/datacenters", (*HTTPServer).CatalogDatacenters)
registerEndpoint("/v1/catalog/nodes", (*HTTPServer).CatalogNodes)
registerEndpoint("/v1/catalog/services", (*HTTPServer).CatalogServices)
registerEndpoint("/v1/catalog/service/", (*HTTPServer).CatalogServiceNodes)
registerEndpoint("/v1/catalog/node/", (*HTTPServer).CatalogNodeServices)
registerEndpoint("/v1/coordinate/datacenters", (*HTTPServer).CoordinateDatacenters)
registerEndpoint("/v1/coordinate/nodes", (*HTTPServer).CoordinateNodes)
registerEndpoint("/v1/coordinate/node/", (*HTTPServer).CoordinateNode)
registerEndpoint("/v1/coordinate/update", (*HTTPServer).CoordinateUpdate)
registerEndpoint("/v1/event/fire/", (*HTTPServer).EventFire)
registerEndpoint("/v1/event/list", (*HTTPServer).EventList)
registerEndpoint("/v1/health/node/", (*HTTPServer).HealthNodeChecks)
registerEndpoint("/v1/health/checks/", (*HTTPServer).HealthServiceChecks)
registerEndpoint("/v1/health/state/", (*HTTPServer).HealthChecksInState)
registerEndpoint("/v1/health/service/", (*HTTPServer).HealthServiceNodes)
registerEndpoint("/v1/internal/ui/nodes", (*HTTPServer).UINodes)
registerEndpoint("/v1/internal/ui/node/", (*HTTPServer).UINodeInfo)
registerEndpoint("/v1/internal/ui/services", (*HTTPServer).UIServices)
registerEndpoint("/v1/kv/", (*HTTPServer).KVSEndpoint)
registerEndpoint("/v1/operator/raft/configuration", (*HTTPServer).OperatorRaftConfiguration)
registerEndpoint("/v1/operator/raft/peer", (*HTTPServer).OperatorRaftPeer)
registerEndpoint("/v1/operator/keyring", (*HTTPServer).OperatorKeyringEndpoint)
registerEndpoint("/v1/operator/autopilot/configuration", (*HTTPServer).OperatorAutopilotConfiguration)
registerEndpoint("/v1/operator/autopilot/health", (*HTTPServer).OperatorServerHealth)
registerEndpoint("/v1/query", (*HTTPServer).PreparedQueryGeneral)
registerEndpoint("/v1/query/", (*HTTPServer).PreparedQuerySpecific)
registerEndpoint("/v1/session/create", (*HTTPServer).SessionCreate)
registerEndpoint("/v1/session/destroy/", (*HTTPServer).SessionDestroy)
registerEndpoint("/v1/session/renew/", (*HTTPServer).SessionRenew)
registerEndpoint("/v1/session/info/", (*HTTPServer).SessionGet)
registerEndpoint("/v1/session/node/", (*HTTPServer).SessionsForNode)
registerEndpoint("/v1/session/list", (*HTTPServer).SessionList)
registerEndpoint("/v1/status/leader", (*HTTPServer).StatusLeader)
registerEndpoint("/v1/status/peers", (*HTTPServer).StatusPeers)
registerEndpoint("/v1/snapshot", (*HTTPServer).Snapshot)
registerEndpoint("/v1/txn", (*HTTPServer).Txn)
}

113
agent/http_oss_test.go Normal file
View File

@ -0,0 +1,113 @@
package agent
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/hashicorp/consul/logger"
)
func TestHTTPAPI_MethodNotAllowed_OSS(t *testing.T) {
tests := []struct {
methods, uri string
}{
{"PUT", "/v1/acl/bootstrap"},
{"PUT", "/v1/acl/create"},
{"PUT", "/v1/acl/update"},
{"PUT", "/v1/acl/destroy/"},
{"GET", "/v1/acl/info/"},
{"PUT", "/v1/acl/clone/"},
{"GET", "/v1/acl/list"},
{"GET", "/v1/acl/replication"},
{"PUT", "/v1/agent/token/"},
{"GET", "/v1/agent/self"},
{"GET", "/v1/agent/members"},
{"PUT", "/v1/agent/check/deregister/"},
{"PUT", "/v1/agent/check/fail/"},
{"PUT", "/v1/agent/check/pass/"},
{"PUT", "/v1/agent/check/register"},
{"PUT", "/v1/agent/check/update/"},
{"PUT", "/v1/agent/check/warn/"},
{"GET", "/v1/agent/checks"},
{"PUT", "/v1/agent/force-leave/"},
{"PUT", "/v1/agent/join/"},
{"PUT", "/v1/agent/leave"},
{"PUT", "/v1/agent/maintenance"},
{"GET", "/v1/agent/metrics"},
// {"GET", "/v1/agent/monitor"}, // requires LogWriter. Hangs if LogWriter is provided
{"PUT", "/v1/agent/reload"},
{"PUT", "/v1/agent/service/deregister/"},
{"PUT", "/v1/agent/service/maintenance/"},
{"PUT", "/v1/agent/service/register"},
{"GET", "/v1/agent/services"},
{"GET", "/v1/catalog/datacenters"},
{"PUT", "/v1/catalog/deregister"},
{"GET", "/v1/catalog/node/"},
{"GET", "/v1/catalog/nodes"},
{"PUT", "/v1/catalog/register"},
{"GET", "/v1/catalog/service/"},
{"GET", "/v1/catalog/services"},
{"GET", "/v1/coordinate/datacenters"},
{"GET", "/v1/coordinate/nodes"},
{"GET", "/v1/coordinate/node/"},
{"PUT", "/v1/event/fire/"},
{"GET", "/v1/event/list"},
{"GET", "/v1/health/checks/"},
{"GET", "/v1/health/node/"},
{"GET", "/v1/health/service/"},
{"GET", "/v1/health/state/"},
{"GET", "/v1/internal/ui/node/"},
{"GET", "/v1/internal/ui/nodes"},
{"GET", "/v1/internal/ui/services"},
{"GET PUT DELETE", "/v1/kv/"},
{"GET PUT", "/v1/operator/autopilot/configuration"},
{"GET", "/v1/operator/autopilot/health"},
{"GET POST PUT DELETE", "/v1/operator/keyring"},
{"GET", "/v1/operator/raft/configuration"},
{"DELETE", "/v1/operator/raft/peer"},
{"GET POST", "/v1/query"},
{"GET PUT DELETE", "/v1/query/"},
{"GET", "/v1/query/xxx/execute"},
{"GET", "/v1/query/xxx/explain"},
{"PUT", "/v1/session/create"},
{"PUT", "/v1/session/destroy/"},
{"GET", "/v1/session/info/"},
{"GET", "/v1/session/list"},
{"GET", "/v1/session/node/"},
{"PUT", "/v1/session/renew/"},
{"GET PUT", "/v1/snapshot"},
{"GET", "/v1/status/leader"},
// {"GET", "/v1/status/peers"},// hangs
{"PUT", "/v1/txn"},
}
a := NewTestAgent(t.Name(), `acl_datacenter = "dc1"`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
all := []string{"GET", "PUT", "POST", "DELETE", "HEAD"}
client := http.Client{}
for _, tt := range tests {
for _, m := range all {
t.Run(m+" "+tt.uri, func(t *testing.T) {
uri := fmt.Sprintf("http://%s%s", a.HTTPAddr(), tt.uri)
req, _ := http.NewRequest(m, uri, nil)
resp, err := client.Do(req)
if err != nil {
t.Fatal("client.Do failed: ", err)
}
allowed := strings.Contains(tt.methods, m)
if allowed && resp.StatusCode == http.StatusMethodNotAllowed {
t.Fatalf("method allowed: got status code %d want any other code", resp.StatusCode)
}
if !allowed && resp.StatusCode != http.StatusMethodNotAllowed {
t.Fatalf("method not allowed: got status code %d want %d", resp.StatusCode, http.StatusMethodNotAllowed)
}
})
}
}
}

View File

@ -20,7 +20,6 @@ import (
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/testutil"
"github.com/hashicorp/go-cleanhttp"
"golang.org/x/net/http2"
@ -390,115 +389,6 @@ func TestHTTPAPIResponseHeaders(t *testing.T) {
}
}
func TestHTTPAPI_MethodNotAllowed(t *testing.T) {
tests := []struct {
methods, uri string
}{
{"PUT", "/v1/acl/bootstrap"},
{"PUT", "/v1/acl/create"},
{"PUT", "/v1/acl/update"},
{"PUT", "/v1/acl/destroy/"},
{"GET", "/v1/acl/info/"},
{"PUT", "/v1/acl/clone/"},
{"GET", "/v1/acl/list"},
{"GET", "/v1/acl/replication"},
{"PUT", "/v1/agent/token/"},
{"GET", "/v1/agent/self"},
{"GET", "/v1/agent/members"},
{"PUT", "/v1/agent/check/deregister/"},
{"PUT", "/v1/agent/check/fail/"},
{"PUT", "/v1/agent/check/pass/"},
{"PUT", "/v1/agent/check/register"},
{"PUT", "/v1/agent/check/update/"},
{"PUT", "/v1/agent/check/warn/"},
{"GET", "/v1/agent/checks"},
{"PUT", "/v1/agent/force-leave/"},
{"PUT", "/v1/agent/join/"},
{"PUT", "/v1/agent/leave"},
{"PUT", "/v1/agent/maintenance"},
{"GET", "/v1/agent/metrics"},
// {"GET", "/v1/agent/monitor"}, // requires LogWriter. Hangs if LogWriter is provided
{"PUT", "/v1/agent/reload"},
{"PUT", "/v1/agent/service/deregister/"},
{"PUT", "/v1/agent/service/maintenance/"},
{"PUT", "/v1/agent/service/register"},
{"GET", "/v1/agent/services"},
{"GET", "/v1/catalog/datacenters"},
{"PUT", "/v1/catalog/deregister"},
{"GET", "/v1/catalog/node/"},
{"GET", "/v1/catalog/nodes"},
{"PUT", "/v1/catalog/register"},
{"GET", "/v1/catalog/service/"},
{"GET", "/v1/catalog/services"},
{"GET", "/v1/coordinate/datacenters"},
{"GET", "/v1/coordinate/nodes"},
{"GET", "/v1/coordinate/node/"},
{"PUT", "/v1/event/fire/"},
{"GET", "/v1/event/list"},
{"GET", "/v1/health/checks/"},
{"GET", "/v1/health/node/"},
{"GET", "/v1/health/service/"},
{"GET", "/v1/health/state/"},
{"GET", "/v1/internal/ui/node/"},
{"GET", "/v1/internal/ui/nodes"},
{"GET", "/v1/internal/ui/services"},
{"GET PUT DELETE", "/v1/kv/"},
{"GET PUT", "/v1/operator/autopilot/configuration"},
{"GET", "/v1/operator/autopilot/health"},
{"GET POST PUT DELETE", "/v1/operator/keyring"},
{"GET", "/v1/operator/raft/configuration"},
{"DELETE", "/v1/operator/raft/peer"},
{"GET POST", "/v1/query"},
{"GET PUT DELETE", "/v1/query/"},
{"GET", "/v1/query/xxx/execute"},
{"GET", "/v1/query/xxx/explain"},
{"PUT", "/v1/session/create"},
{"PUT", "/v1/session/destroy/"},
{"GET", "/v1/session/info/"},
{"GET", "/v1/session/list"},
{"GET", "/v1/session/node/"},
{"PUT", "/v1/session/renew/"},
{"GET PUT", "/v1/snapshot"},
{"GET", "/v1/status/leader"},
// {"GET", "/v1/status/peers"},// hangs
{"PUT", "/v1/txn"},
// enterprise only
// {"GET POST", "/v1/operator/area"},
// {"GET PUT DELETE", "/v1/operator/area/"},
// {"GET", "/v1/operator/area/xxx/members"},
}
a := NewTestAgent(t.Name(), `acl_datacenter = "dc1"`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
all := []string{"GET", "PUT", "POST", "DELETE", "HEAD"}
client := http.Client{}
for _, tt := range tests {
for _, m := range all {
t.Run(m+" "+tt.uri, func(t *testing.T) {
uri := fmt.Sprintf("http://%s%s", a.HTTPAddr(), tt.uri)
req, _ := http.NewRequest(m, uri, nil)
resp, err := client.Do(req)
if err != nil {
t.Fatal("client.Do failed: ", err)
}
allowed := strings.Contains(tt.methods, m)
if allowed && resp.StatusCode == http.StatusMethodNotAllowed {
t.Fatalf("method allowed: got status code %d want any other code", resp.StatusCode)
}
if !allowed && resp.StatusCode != http.StatusMethodNotAllowed {
t.Fatalf("method not allowed: got status code %d want %d", resp.StatusCode, http.StatusMethodNotAllowed)
}
})
}
}
}
func TestContentTypeIsJSON(t *testing.T) {
t.Parallel()
a := NewTestAgent(t.Name(), "")