Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation"
"sigs.k8s.io/controller-runtime/pkg/client"
)

Expand Down Expand Up @@ -130,6 +131,7 @@ type AdkApiTranslator interface {
inputs *AgentManifestInputs,
) (*AgentOutputs, error)
GetOwnedResourceTypes() []client.Object
WithClusterDomain(clusterDomain string) AdkApiTranslator
}

// probeConfig holds readiness probe timing configuration
Expand Down Expand Up @@ -177,16 +179,30 @@ func NewAdkApiTranslatorWithWatchedNamespaces(kube client.Client, watchedNamespa
defaultModelConfig: defaultModelConfig,
plugins: plugins,
globalProxyURL: globalProxyURL,
clusterDomain: "cluster.local",
sandboxBackend: sandboxBackend,
}
}

func (a *adkApiTranslator) WithClusterDomain(clusterDomain string) AdkApiTranslator {
if a == nil {
return a
}
clusterDomain = strings.TrimSpace(clusterDomain)
if clusterDomain == "" {
clusterDomain = "cluster.local"
}
a.clusterDomain = clusterDomain
return a
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: prefer using a != nil instead for slightly better readability

Suggested change
return a
if a != nil {
clusterDomain = strings.TrimSpace(clusterDomain)
if clusterDomain == "" {
clusterDomain = "cluster.local"
}
a.clusterDomain = clusterDomain
}
return a

}

type adkApiTranslator struct {
kube client.Client
watchedNamespaces []string
defaultModelConfig types.NamespacedName
plugins []TranslatorPlugin
globalProxyURL string
clusterDomain string
sandboxBackend sandboxbackend.Backend
}

Expand Down Expand Up @@ -953,34 +969,68 @@ func (a *adkApiTranslator) isInternalK8sURL(ctx context.Context, urlStr, namespa
return false
}

// Check if it ends with .svc.cluster.local (definitely internal)
if strings.HasSuffix(hostname, ".svc.cluster.local") {
clusterDomain := strings.TrimSpace(a.clusterDomain)
if clusterDomain == "" {
clusterDomain = "cluster.local"
}
svcDomain := ".svc." + clusterDomain

// Only treat fully qualified service hostnames as internal if they match
// a valid Kubernetes service DNS pattern for the current cluster domain.
// Valid forms are:
// service.namespace.svc.<clusterDomain>
// service.namespace.svc
// service.namespace
if strings.HasSuffix(hostname, svcDomain) {
prefix := strings.TrimSuffix(hostname, svcDomain)
if prefix == "" {
return false
}
parts := strings.Split(prefix, ".")
if len(parts) != 2 {
return false
}
if len(validation.IsDNS1123Label(parts[0])) > 0 || len(validation.IsDNS1123Label(parts[1])) > 0 {
return false
}
return true
}

// Extract namespace from hostname pattern: {name}.{namespace}
// Examples: test-mcp-server.kagent -> namespace is "kagent"
Comment on lines -961 to -962
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we keep this comment? this seems to still be relevant so it would be good context.

parts := strings.Split(hostname, ".")
if len(parts) == 2 {
service := parts[0]
potentialNamespace := parts[1]

// Check if this namespace exists in the cluster
ns := &corev1.Namespace{}
err := a.kube.Get(ctx, types.NamespacedName{Name: potentialNamespace}, ns)
if err == nil {
// Namespace exists, so this is an internal k8s URL
return true
if len(validation.IsDNS1123Label(service)) > 0 || len(validation.IsDNS1123Label(potentialNamespace)) > 0 {
return false
}
// Controller is using namespaced RBAC, so check if the namespace is watched
if (apierrors.IsForbidden(err) || apierrors.IsUnauthorized(err)) && len(a.watchedNamespaces) > 0 {
return slices.Contains(a.watchedNamespaces, potentialNamespace)
return a.namespaceExistsOrWatched(ctx, potentialNamespace)
}

if len(parts) == 3 && parts[2] == "svc" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would be helpful to add a comment that this conditional specifically addressesing a hostname using the grafana.kagent.svc construction

service := parts[0]
potentialNamespace := parts[1]
if len(validation.IsDNS1123Label(service)) > 0 || len(validation.IsDNS1123Label(potentialNamespace)) > 0 {
return false
}
// If namespace doesn't exist, it's likely a TLD or external domain
return a.namespaceExistsOrWatched(ctx, potentialNamespace)
}

return false
}

func (a *adkApiTranslator) namespaceExistsOrWatched(ctx context.Context, potentialNamespace string) bool {
ns := &corev1.Namespace{}
err := a.kube.Get(ctx, types.NamespacedName{Name: potentialNamespace}, ns)
if err == nil {
return true
}
if (apierrors.IsForbidden(err) || apierrors.IsUnauthorized(err)) && len(a.watchedNamespaces) > 0 {
return slices.Contains(a.watchedNamespaces, potentialNamespace)
}
return false
}


func applyProxyURL(originalURL, proxyURL string, headers map[string]string) (targetURL string, updatedHeaders map[string]string, err error) {
// Parse original URL to extract path and hostname
originalURLParsed, err := url.Parse(originalURL)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package agent

import (
"context"
"testing"

"github.com/stretchr/testify/require"
"github.com/kagent-dev/kagent/go/api/v1alpha2"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestAdkApiTranslator_IsInternalK8sURL(t *testing.T) {
scheme := k8sruntime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
require.NoError(t, v1alpha2.AddToScheme(scheme))

namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: "kagent"},
}

kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(namespace).Build()
translatorImpl := NewAdkApiTranslatorWithWatchedNamespaces(kubeClient, nil, types.NamespacedName{Name: "default-model"}, nil, "", nil).WithClusterDomain("cluster.local").(*adkApiTranslator)

require.True(t, translatorImpl.isInternalK8sURL(context.Background(), "http://grafana.kagent.svc.cluster.local:3000/api", "kagent"), "should recognize fully qualified service DNS as internal")
require.True(t, translatorImpl.isInternalK8sURL(context.Background(), "http://grafana.kagent.svc:3000/api", "kagent"), "should recognize service.namespace.svc shorthand as internal")
require.True(t, translatorImpl.isInternalK8sURL(context.Background(), "http://grafana.kagent:3000/api", "kagent"), "should recognize service.namespace shorthand as internal")

require.False(t, translatorImpl.isInternalK8sURL(context.Background(), "http://grafana.kagent.svc.cluster.local.evil.com:3000/api", "kagent"), "should reject external domains that only contain the cluster suffix")
require.False(t, translatorImpl.isInternalK8sURL(context.Background(), "http://example.com:8080", "kagent"), "should reject normal external URLs")

translatorImpl.WithClusterDomain("custom.internal")
require.True(t, translatorImpl.isInternalK8sURL(context.Background(), "http://grafana.kagent.svc.custom.internal:3000/api", "kagent"), "should honor custom cluster-domain values")
}
2 changes: 1 addition & 1 deletion go/core/internal/httpserver/handlers/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ func (h *AgentsHandler) buildTranslator(kubeClient client.Client) agent_translat
nil,
h.ProxyURL,
h.SandboxBackend,
)
).WithClusterDomain(h.ClusterDomain)
}

func (h *AgentsHandler) validateAgentObject(ctx context.Context, agent v1alpha2.AgentObject) error {
Expand Down
4 changes: 3 additions & 1 deletion go/core/internal/httpserver/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,20 @@ type Base struct {
DatabaseService database.Client
Authorizer auth.Authorizer // Interface for authorization checks
ProxyURL string
ClusterDomain string
WatchedNamespaces []string
SandboxBackend sandboxbackend.Backend
}

// NewHandlers creates a new Handlers instance with all handler components.
func NewHandlers(kubeClient client.Client, defaultModelConfig types.NamespacedName, dbService database.Client, watchedNamespaces []string, authorizer auth.Authorizer, proxyURL string, rcnclr reconciler.KagentReconciler, sandboxBackend sandboxbackend.Backend) *Handlers {
func NewHandlers(kubeClient client.Client, defaultModelConfig types.NamespacedName, dbService database.Client, watchedNamespaces []string, authorizer auth.Authorizer, proxyURL string, clusterDomain string, rcnclr reconciler.KagentReconciler, sandboxBackend sandboxbackend.Backend) *Handlers {
base := &Base{
KubeClient: kubeClient,
DefaultModelConfig: defaultModelConfig,
DatabaseService: dbService,
Authorizer: authorizer,
ProxyURL: proxyURL,
ClusterDomain: clusterDomain,
WatchedNamespaces: watchedNamespaces,
SandboxBackend: sandboxBackend,
}
Expand Down
3 changes: 2 additions & 1 deletion go/core/internal/httpserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ type ServerConfig struct {
Authenticator auth.AuthProvider
Authorizer auth.Authorizer
ProxyURL string
ClusterDomain string
Reconciler reconciler.KagentReconciler
SandboxBackend sandboxbackend.Backend
}
Expand All @@ -89,7 +90,7 @@ func NewHTTPServer(config ServerConfig) (*HTTPServer, error) {
return &HTTPServer{
config: config,
router: config.Router,
handlers: handlers.NewHandlers(config.KubeClient, defaultModelConfig, config.DbClient, config.WatchedNamespaces, config.Authorizer, config.ProxyURL, config.Reconciler, config.SandboxBackend),
handlers: handlers.NewHandlers(config.KubeClient, defaultModelConfig, config.DbClient, config.WatchedNamespaces, config.Authorizer, config.ProxyURL, config.ClusterDomain, config.Reconciler, config.SandboxBackend),
authenticator: config.Authenticator,
}, nil
}
Expand Down
4 changes: 3 additions & 1 deletion go/core/pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ type Config struct {
HttpServerAddr string
WatchNamespaces string
A2ABaseUrl string
ClusterDomain string
Database struct {
Url string
UrlFile string
Expand Down Expand Up @@ -174,6 +175,7 @@ func (cfg *Config) SetFlags(commandLine *flag.FlagSet) {
commandLine.StringVar(&cfg.DefaultModelConfig.Namespace, "default-model-config-namespace", kagentNamespace, "The namespace of the default model config.")
commandLine.StringVar(&cfg.HttpServerAddr, "http-server-address", ":8083", "The address the HTTP server binds to.")
commandLine.StringVar(&cfg.A2ABaseUrl, "a2a-base-url", "http://127.0.0.1:8083", "The base URL of the A2A Server endpoint, as advertised to clients.")
commandLine.StringVar(&cfg.ClusterDomain, "cluster-domain", "cluster.local", "The Kubernetes cluster DNS domain used for internal service discovery.")
commandLine.StringVar(&cfg.Database.Url, "postgres-database-url", "postgres://postgres:kagent@kagent-postgresql.kagent.svc.cluster.local:5432/postgres", "The URL of the PostgreSQL database.")
commandLine.StringVar(&cfg.Database.UrlFile, "postgres-database-url-file", "", "Path to a file containing the PostgreSQL database URL. Takes precedence over --postgres-database-url.")
commandLine.BoolVar(&cfg.Database.VectorEnabled, "database-vector-enabled", true, "Enable pgvector extension and memory table. Requires pgvector to be installed on the PostgreSQL server.")
Expand Down Expand Up @@ -518,7 +520,7 @@ func Start(getExtensionConfig GetExtensionConfig, migrationRunner MigrationRunne
extensionCfg.AgentPlugins,
cfg.Proxy.URL,
extensionCfg.SandboxBackend,
)
).WithClusterDomain(cfg.ClusterDomain)

rcnclr := reconciler.NewKagentReconciler(
apiTranslator,
Expand Down
2 changes: 1 addition & 1 deletion helm/kagent/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ cilium-debug-agent:
grafana-mcp:
enabled: true
grafana:
url: "grafana.kagent:3000/api"
url: "http://grafana.kagent.svc.cluster.local:3000/api"
serviceAccountToken: ""
# apiKey: "" # Deprecated - use serviceAccountToken instead.
# secretRef: "" # Name of Secret to reference (contains GRAFANA_SERVICE_ACCOUNT_TOKEN or GRAFANA_API_KEY)
Expand Down
2 changes: 1 addition & 1 deletion helm/tools/grafana-mcp/values.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
replicas: 1

grafana:
url: "grafana.kagent:3000/api"
url: "http://grafana.kagent.svc.cluster.local:3000/api"
serviceAccountToken: ""
# apiKey: "" # Deprecated - use serviceAccountToken instead.
# secretRef: "" # Name of Secret to reference (contains GRAFANA_SERVICE_ACCOUNT_TOKEN or GRAFANA_API_KEY)
Expand Down