diff --git a/api/types/constants.go b/api/types/constants.go index 1424dca361c97..1f1fa410fed24 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -847,6 +847,8 @@ const ( DiscoveryAppRewriteLabel = TeleportNamespace + "/app-rewrite" // DiscoveryAppNameLabel specifies explicitly name of an app created from Kubernetes service. DiscoveryAppNameLabel = TeleportNamespace + "/name" + // DiscoveryPathLabel optionally specifies a context path for apps created from Kubernetes services. + DiscoveryPathLabel = TeleportNamespace + "/path" // DiscoveryAppInsecureSkipVerify specifies the TLS verification enforcement for a discovered app created from Kubernetes service. DiscoveryAppInsecureSkipVerify = TeleportNamespace + "/insecure-skip-verify" // DiscoveryAppIgnore specifies if a Kubernetes service should be ignored by discovery service. diff --git a/docs/pages/reference/agent-services/kubernetes-application-discovery.mdx b/docs/pages/reference/agent-services/kubernetes-application-discovery.mdx index 85ea88ebbd6eb..d0bb7606e1f67 100644 --- a/docs/pages/reference/agent-services/kubernetes-application-discovery.mdx +++ b/docs/pages/reference/agent-services/kubernetes-application-discovery.mdx @@ -150,3 +150,13 @@ Controls the public address for the Teleport app we create if needed. annotations: teleport.dev/public-addr: "custom.teleport.dev" ``` + +### `teleport.dev/path` + +The path is appended to the URI generated for the Teleport app for cases where +an application is served on a sub-path of an HTTP service. + +```yaml +annotations: + teleport.dev/path: "foo/bar" +``` diff --git a/lib/service/service.go b/lib/service/service.go index 06c9aac16faa9..7ee97bafac55d 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -2015,6 +2015,8 @@ func (process *TeleportProcess) initAuthService() error { } process.backend = b + // TODO(zmb3) maybe check here + var emitter apievents.Emitter var streamer events.Streamer var uploadHandler events.MultipartHandler @@ -6316,6 +6318,8 @@ func (process *TeleportProcess) initAuthStorage() (backend.Backend, error) { return nil, trace.Wrap(err) } + // TODO(zmb3): bk (if firestore or dynamo) should not use same table as recordings + reporter, err := backend.NewReporter(backend.ReporterConfig{ Component: teleport.ComponentBackend, Backend: backend.NewSanitizer(bk), diff --git a/lib/services/app.go b/lib/services/app.go index dd46c37bc7661..112a254f46c0c 100644 --- a/lib/services/app.go +++ b/lib/services/app.go @@ -164,7 +164,7 @@ func UnmarshalAppServer(data []byte, opts ...MarshalOption) (types.AppServer, er // It transforms service fields and annotations into appropriate Teleport app fields. // Service labels are copied to app labels. func NewApplicationFromKubeService(service corev1.Service, clusterName, protocol string, port corev1.ServicePort) (types.Application, error) { - appURI := buildAppURI(protocol, GetServiceFQDN(service), port.Port) + appURI := buildAppURI(protocol, GetServiceFQDN(service), service.GetAnnotations()[types.DiscoveryPathLabel], port.Port) rewriteConfig, err := getAppRewriteConfig(service.GetAnnotations()) if err != nil { @@ -210,10 +210,11 @@ func GetServiceFQDN(service corev1.Service) string { return fmt.Sprintf("%s.%s.svc.%s", service.GetName(), service.GetNamespace(), clusterDomainResolver()) } -func buildAppURI(protocol, serviceFQDN string, port int32) string { +func buildAppURI(protocol, serviceFQDN, path string, port int32) string { return (&url.URL{ Scheme: protocol, Host: fmt.Sprintf("%s:%d", serviceFQDN, port), + Path: path, }).String() } diff --git a/lib/services/app_test.go b/lib/services/app_test.go index dc79a7426a65f..2dadd3fdb6cf6 100644 --- a/lib/services/app_test.go +++ b/lib/services/app_test.go @@ -211,6 +211,7 @@ func TestBuildAppURI(t *testing.T) { tests := []struct { serviceFQDN string port int32 + path string protocol string expected string }{ @@ -220,6 +221,34 @@ func TestBuildAppURI(t *testing.T) { protocol: "http", expected: "http://service.example:8080", }, + { + serviceFQDN: "service.example", + port: 8080, + protocol: "http", + path: "foo", + expected: "http://service.example:8080/foo", + }, + { + serviceFQDN: "service.example", + port: 8080, + protocol: "http", + path: "/foo", + expected: "http://service.example:8080/foo", + }, + { + serviceFQDN: "service.example", + port: 8080, + protocol: "http", + path: "foo/bar", + expected: "http://service.example:8080/foo/bar", + }, + { + serviceFQDN: "service.example", + port: 8080, + protocol: "http", + path: "foo bar", + expected: "http://service.example:8080/foo%20bar", + }, { serviceFQDN: "service.example", port: 443, @@ -235,7 +264,7 @@ func TestBuildAppURI(t *testing.T) { } for _, tt := range tests { - require.Equal(t, tt.expected, buildAppURI(tt.protocol, tt.serviceFQDN, tt.port)) + require.Equal(t, tt.expected, buildAppURI(tt.protocol, tt.serviceFQDN, tt.path, tt.port)) } } diff --git a/lib/srv/discovery/discovery_test.go b/lib/srv/discovery/discovery_test.go index 4b276f19dd9c8..46d4e89b205ef 100644 --- a/lib/srv/discovery/discovery_test.go +++ b/lib/srv/discovery/discovery_test.go @@ -24,9 +24,11 @@ import ( "errors" "fmt" "io" + "net/url" "os" "regexp" "slices" + "strings" "sync" "testing" "time" @@ -1197,12 +1199,17 @@ func TestDiscoveryKubeServices(t *testing.T) { appProtocolHTTP := "http" mockKubeServices := []*corev1.Service{ - newMockKubeService("service1", "ns1", "", map[string]string{"test-label": "testval"}, map[string]string{types.DiscoveryPublicAddr: "custom.example.com"}, + newMockKubeService("service1", "ns1", "", + map[string]string{"test-label": "testval"}, + map[string]string{types.DiscoveryPublicAddr: "custom.example.com", types.DiscoveryPathLabel: "foo/bar baz"}, []corev1.ServicePort{{Port: 42, Name: "http", Protocol: corev1.ProtocolTCP}}), - newMockKubeService("service2", "ns2", "", map[string]string{ - "test-label": "testval", - "test-label2": "testval2", - }, nil, []corev1.ServicePort{{Port: 42, Name: "custom", AppProtocol: &appProtocolHTTP, Protocol: corev1.ProtocolTCP}}), + newMockKubeService("service2", "ns2", "", + map[string]string{ + "test-label": "testval", + "test-label2": "testval2", + }, + nil, + []corev1.ServicePort{{Port: 42, Name: "custom", AppProtocol: &appProtocolHTTP, Protocol: corev1.ProtocolTCP}}), } app1 := mustConvertKubeServiceToApp(t, mainDiscoveryGroup, "http", mockKubeServices[0], mockKubeServices[0].Spec.Ports[0]) @@ -2047,6 +2054,13 @@ func mustConvertKubeServiceToApp(t *testing.T, discoveryGroup, protocol string, app, err := services.NewApplicationFromKubeService(*kubeService, discoveryGroup, protocol, port) require.NoError(t, err) require.Equal(t, kubeService.Annotations[types.DiscoveryPublicAddr], app.GetPublicAddr()) + if p, ok := kubeService.Annotations[types.DiscoveryPathLabel]; ok { + components := strings.Split(p, "/") + for i := range components { + components[i] = url.PathEscape(components[i]) + } + require.True(t, strings.HasSuffix(app.GetURI(), "/"+strings.Join(components, "/")), "uri: %v", app.GetURI()) + } app.GetStaticLabels()[types.TeleportInternalDiscoveryGroupName] = discoveryGroup app.GetStaticLabels()[types.OriginLabel] = types.OriginDiscoveryKubernetes