Skip to content

Commit 82ce421

Browse files
committed
addressed comments
1 parent 4999ab2 commit 82ce421

File tree

12 files changed

+678
-486
lines changed

12 files changed

+678
-486
lines changed

cmd/listener.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,21 @@ var listenCmd = &cobra.Command{
107107
// Create the appropriate reconciler based on configuration
108108
var reconcilerInstance reconciler.CustomReconciler
109109
if appCfg.EnableKcp {
110-
kcpReconciler, err := kcp.NewKCPReconciler(appCfg, reconcilerOpts, log)
110+
kcpManager, err := kcp.NewKCPManager(appCfg, reconcilerOpts, log)
111111
if err != nil {
112-
log.Fatal().Err(err).Msg("unable to create KCP reconciler")
112+
log.Fatal().Err(err).Msg("unable to create KCP manager")
113113
}
114114

115115
// Start virtual workspace watching if path is configured
116116
if appCfg.Listener.VirtualWorkspacesConfigPath != "" {
117117
go func() {
118-
if err := kcpReconciler.StartVirtualWorkspaceWatching(ctx, appCfg.Listener.VirtualWorkspacesConfigPath); err != nil {
118+
if err := kcpManager.StartVirtualWorkspaceWatching(ctx, appCfg.Listener.VirtualWorkspacesConfigPath); err != nil {
119119
log.Fatal().Err(err).Msg("failed to start virtual workspace watching")
120120
}
121121
}()
122122
}
123123

124-
reconcilerInstance = kcpReconciler
124+
reconcilerInstance = kcpManager
125125
} else {
126126
ioHandler, err := workspacefile.NewIOHandler(appCfg.OpenApiDefinitionsPath)
127127
if err != nil {

common/cluster/interfaces.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package cluster
2+
3+
import (
4+
"k8s.io/apimachinery/pkg/api/meta"
5+
"k8s.io/client-go/discovery"
6+
"k8s.io/client-go/rest"
7+
"sigs.k8s.io/controller-runtime/pkg/client"
8+
"sigs.k8s.io/controller-runtime/pkg/cluster"
9+
)
10+
11+
// Cluster represents a unified interface for accessing Kubernetes clusters
12+
// This interface works with both multicluster runtime clusters and standalone clusters
13+
type Cluster interface {
14+
// GetName returns the cluster name/identifier
15+
GetName() string
16+
17+
// GetClient returns a controller-runtime client for this cluster
18+
GetClient() client.Client
19+
20+
// GetConfig returns the rest.Config for this cluster
21+
GetConfig() *rest.Config
22+
23+
// GetRESTMapper returns the REST mapper for this cluster
24+
GetRESTMapper() meta.RESTMapper
25+
26+
// GetDiscoveryClient returns a discovery client for this cluster
27+
GetDiscoveryClient() (discovery.DiscoveryInterface, error)
28+
}
29+
30+
// MulticlusterRuntimeCluster wraps a controller-runtime cluster.Cluster
31+
// to implement our unified Cluster interface
32+
type MulticlusterRuntimeCluster struct {
33+
name string
34+
cluster cluster.Cluster
35+
}
36+
37+
// NewMulticlusterRuntimeCluster creates a new wrapper for multicluster runtime clusters
38+
func NewMulticlusterRuntimeCluster(name string, cluster cluster.Cluster) *MulticlusterRuntimeCluster {
39+
return &MulticlusterRuntimeCluster{
40+
name: name,
41+
cluster: cluster,
42+
}
43+
}
44+
45+
func (c *MulticlusterRuntimeCluster) GetName() string {
46+
return c.name
47+
}
48+
49+
func (c *MulticlusterRuntimeCluster) GetClient() client.Client {
50+
return c.cluster.GetClient()
51+
}
52+
53+
func (c *MulticlusterRuntimeCluster) GetConfig() *rest.Config {
54+
return c.cluster.GetConfig()
55+
}
56+
57+
func (c *MulticlusterRuntimeCluster) GetRESTMapper() meta.RESTMapper {
58+
return c.cluster.GetRESTMapper()
59+
}
60+
61+
func (c *MulticlusterRuntimeCluster) GetDiscoveryClient() (discovery.DiscoveryInterface, error) {
62+
return discovery.NewDiscoveryClientForConfig(c.cluster.GetConfig())
63+
}
64+
65+
// StandaloneCluster represents a standalone cluster (non-multicluster runtime)
66+
type StandaloneCluster struct {
67+
name string
68+
client client.Client
69+
config *rest.Config
70+
mapper meta.RESTMapper
71+
discovery discovery.DiscoveryInterface
72+
}
73+
74+
// NewStandaloneCluster creates a new standalone cluster
75+
func NewStandaloneCluster(name string, client client.Client, config *rest.Config, mapper meta.RESTMapper) (*StandaloneCluster, error) {
76+
discovery, err := discovery.NewDiscoveryClientForConfig(config)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
return &StandaloneCluster{
82+
name: name,
83+
client: client,
84+
config: config,
85+
mapper: mapper,
86+
discovery: discovery,
87+
}, nil
88+
}
89+
90+
func (c *StandaloneCluster) GetName() string {
91+
return c.name
92+
}
93+
94+
func (c *StandaloneCluster) GetClient() client.Client {
95+
return c.client
96+
}
97+
98+
func (c *StandaloneCluster) GetConfig() *rest.Config {
99+
return c.config
100+
}
101+
102+
func (c *StandaloneCluster) GetRESTMapper() meta.RESTMapper {
103+
return c.mapper
104+
}
105+
106+
func (c *StandaloneCluster) GetDiscoveryClient() (discovery.DiscoveryInterface, error) {
107+
return c.discovery, nil
108+
}

common/cluster/manager.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package cluster
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
8+
)
9+
10+
// Manager provides unified access to clusters across different runtime modes
11+
type Manager interface {
12+
// GetCluster returns a cluster by name
13+
GetCluster(ctx context.Context, name string) (Cluster, error)
14+
15+
// ListClusters returns all available clusters
16+
ListClusters(ctx context.Context) ([]string, error)
17+
18+
// IsMulticluster returns true if this manager uses multicluster runtime
19+
IsMulticluster() bool
20+
}
21+
22+
// MulticlusterManager wraps multicluster runtime manager
23+
type MulticlusterManager struct {
24+
mcMgr mcmanager.Manager
25+
}
26+
27+
// NewMulticlusterManager creates a new multicluster manager wrapper
28+
func NewMulticlusterManager(mcMgr mcmanager.Manager) *MulticlusterManager {
29+
return &MulticlusterManager{
30+
mcMgr: mcMgr,
31+
}
32+
}
33+
34+
func (m *MulticlusterManager) GetCluster(ctx context.Context, name string) (Cluster, error) {
35+
cluster, err := m.mcMgr.GetCluster(ctx, name)
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to get cluster %s from multicluster manager: %w", name, err)
38+
}
39+
return NewMulticlusterRuntimeCluster(name, cluster), nil
40+
}
41+
42+
func (m *MulticlusterManager) ListClusters(ctx context.Context) ([]string, error) {
43+
// Note: multicluster runtime doesn't provide a direct ListClusters method
44+
// This would need to be implemented based on the specific multicluster runtime version
45+
// For now, return empty list - clusters are discovered dynamically
46+
return []string{}, nil
47+
}
48+
49+
func (m *MulticlusterManager) IsMulticluster() bool {
50+
return true
51+
}
52+
53+
// StandaloneManager manages standalone clusters (legacy mode)
54+
type StandaloneManager struct {
55+
clusters map[string]Cluster
56+
}
57+
58+
// NewStandaloneManager creates a new standalone cluster manager
59+
func NewStandaloneManager() *StandaloneManager {
60+
return &StandaloneManager{
61+
clusters: make(map[string]Cluster),
62+
}
63+
}
64+
65+
func (m *StandaloneManager) GetCluster(ctx context.Context, name string) (Cluster, error) {
66+
cluster, exists := m.clusters[name]
67+
if !exists {
68+
return nil, fmt.Errorf("cluster %s not found", name)
69+
}
70+
return cluster, nil
71+
}
72+
73+
func (m *StandaloneManager) ListClusters(ctx context.Context) ([]string, error) {
74+
names := make([]string, 0, len(m.clusters))
75+
for name := range m.clusters {
76+
names = append(names, name)
77+
}
78+
return names, nil
79+
}
80+
81+
func (m *StandaloneManager) IsMulticluster() bool {
82+
return false
83+
}
84+
85+
// AddCluster adds a cluster to the standalone manager
86+
func (m *StandaloneManager) AddCluster(cluster Cluster) {
87+
m.clusters[cluster.GetName()] = cluster
88+
}
89+
90+
// RemoveCluster removes a cluster from the standalone manager
91+
func (m *StandaloneManager) RemoveCluster(name string) {
92+
delete(m.clusters, name)
93+
}

gateway/manager/manager.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/platform-mesh/golang-commons/logger"
1010
"k8s.io/client-go/rest"
1111

12+
commoncluster "github.com/platform-mesh/kubernetes-graphql-gateway/common/cluster"
1213
appConfig "github.com/platform-mesh/kubernetes-graphql-gateway/common/config"
1314
"github.com/platform-mesh/kubernetes-graphql-gateway/gateway/manager/roundtripper"
1415
"github.com/platform-mesh/kubernetes-graphql-gateway/gateway/manager/targetcluster"
@@ -24,12 +25,26 @@ type Service struct {
2425

2526
// NewGateway creates a new domain-driven Gateway instance
2627
func NewGateway(ctx context.Context, log *logger.Logger, appCfg appConfig.Config) (*Service, error) {
28+
return NewGatewayWithClusterManager(ctx, log, appCfg, nil)
29+
}
30+
31+
// NewGatewayWithClusterManager creates a new Gateway instance with optional multicluster manager
32+
func NewGatewayWithClusterManager(ctx context.Context, log *logger.Logger, appCfg appConfig.Config, clusterMgr commoncluster.Manager) (*Service, error) {
2733
// Create round tripper factory
2834
roundTripperFactory := targetcluster.RoundTripperFactory(func(adminRT http.RoundTripper, tlsConfig rest.TLSClientConfig) http.RoundTripper {
2935
return roundtripper.New(log, appCfg, adminRT, roundtripper.NewUnauthorizedRoundTripper())
3036
})
3137

32-
clusterRegistry := targetcluster.NewClusterRegistry(log, appCfg, roundTripperFactory)
38+
var clusterRegistry ClusterManager
39+
if clusterMgr != nil && clusterMgr.IsMulticluster() {
40+
// Use multicluster-aware cluster registry
41+
clusterRegistry = targetcluster.NewMulticlusterClusterRegistry(log, appCfg, roundTripperFactory, clusterMgr)
42+
log.Info().Msg("Using multicluster-aware cluster registry")
43+
} else {
44+
// Use traditional file-based cluster registry
45+
clusterRegistry = targetcluster.NewClusterRegistry(log, appCfg, roundTripperFactory)
46+
log.Info().Msg("Using traditional file-based cluster registry")
47+
}
3348

3449
schemaWatcher, err := watcher.NewFileWatcher(log, clusterRegistry)
3550
if err != nil {

gateway/manager/targetcluster/cluster.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"sigs.k8s.io/controller-runtime/pkg/client"
1414

1515
"github.com/platform-mesh/kubernetes-graphql-gateway/common/auth"
16+
commoncluster "github.com/platform-mesh/kubernetes-graphql-gateway/common/cluster"
1617
appConfig "github.com/platform-mesh/kubernetes-graphql-gateway/common/config"
1718
"github.com/platform-mesh/kubernetes-graphql-gateway/gateway/resolver"
1819
"github.com/platform-mesh/kubernetes-graphql-gateway/gateway/schema"
@@ -94,6 +95,71 @@ func NewTargetCluster(
9495
return cluster, nil
9596
}
9697

98+
// NewTargetClusterFromMulticluster creates a new TargetCluster using multicluster runtime cluster
99+
func NewTargetClusterFromMulticluster(
100+
name string,
101+
schemaFilePath string,
102+
mcCluster commoncluster.Cluster,
103+
log *logger.Logger,
104+
appCfg appConfig.Config,
105+
roundTripperFactory RoundTripperFactory,
106+
) (*TargetCluster, error) {
107+
log.Info().
108+
Str("cluster", name).
109+
Str("file", schemaFilePath).
110+
Msg("Creating target cluster from multicluster runtime")
111+
112+
cluster := &TargetCluster{
113+
appCfg: appCfg,
114+
name: name,
115+
log: log,
116+
}
117+
118+
// Use multicluster runtime cluster directly
119+
// Note: multicluster runtime client may not implement WithWatch, so we create a new one
120+
cluster.restCfg = mcCluster.GetConfig()
121+
122+
// Create a new WithWatch client using the multicluster runtime config
123+
var err error
124+
cluster.client, err = client.NewWithWatch(cluster.restCfg, client.Options{})
125+
if err != nil {
126+
return nil, fmt.Errorf("failed to create WithWatch client from multicluster config: %w", err)
127+
}
128+
129+
// Apply round tripper factory if provided
130+
if roundTripperFactory != nil {
131+
cluster.restCfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
132+
return roundTripperFactory(rt, cluster.restCfg.TLSClientConfig)
133+
})
134+
}
135+
136+
// Load schema from file (still needed for GraphQL schema generation)
137+
if err = cluster.loadSchemaFromFile(schemaFilePath); err != nil {
138+
return nil, fmt.Errorf("failed to load schema from file: %w", err)
139+
}
140+
141+
log.Info().
142+
Str("cluster", name).
143+
Msg("Successfully created target cluster from multicluster runtime")
144+
145+
return cluster, nil
146+
}
147+
148+
// loadSchemaFromFile loads GraphQL schema from file and creates handler
149+
func (tc *TargetCluster) loadSchemaFromFile(schemaFilePath string) error {
150+
fileData, err := readSchemaFile(schemaFilePath)
151+
if err != nil {
152+
return fmt.Errorf("failed to read schema file: %w", err)
153+
}
154+
155+
// Create GraphQL schema and handler
156+
if err := tc.createHandler(fileData.Definitions, tc.appCfg); err != nil {
157+
return fmt.Errorf("failed to create GraphQL handler: %w", err)
158+
}
159+
160+
return nil
161+
}
162+
97163
// connect establishes connection to the target cluster
98164
func (tc *TargetCluster) connect(appCfg appConfig.Config, metadata *ClusterMetadata, roundTripperFactory func(http.RoundTripper, rest.TLSClientConfig) http.RoundTripper) error {
99165
// All clusters now use metadata from schema files to get kubeconfig

gateway/manager/targetcluster/graphql.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import (
1919
"github.com/platform-mesh/kubernetes-graphql-gateway/gateway/manager/roundtripper"
2020
)
2121

22-
// Context key types to avoid collisions
23-
type LogicalClusterKey struct{}
22+
// LogicalClusterKey is the context key for storing logical cluster information
23+
// Using logicalcluster.Name as the key type to be compatible with KCP ecosystem
24+
type LogicalClusterKey = logicalcluster.Name
2425

2526
// GraphQLHandler wraps a GraphQL schema and HTTP handler
2627
type GraphQLHandler struct {
@@ -65,8 +66,8 @@ func SetContexts(r *http.Request, workspace, token string, enableKcp bool) *http
6566
if kcpWorkspace, ok := r.Context().Value(kcpWorkspaceKey).(string); ok && kcpWorkspace != "" {
6667
kcpWorkspaceName = kcpWorkspace
6768
}
68-
// Store the logical cluster name in context without using KCP-specific kontext
69-
r = r.WithContext(context.WithValue(r.Context(), LogicalClusterKey{}, logicalcluster.Name(kcpWorkspaceName)))
69+
// Store the logical cluster name in context using logicalcluster.Name as key
70+
r = r.WithContext(context.WithValue(r.Context(), LogicalClusterKey("cluster"), logicalcluster.Name(kcpWorkspaceName)))
7071
}
7172
return r.WithContext(context.WithValue(r.Context(), roundtripper.TokenKey{}, token))
7273
}

gateway/manager/targetcluster/graphql_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ func TestSetContexts(t *testing.T) {
196196

197197
// Check KCP context
198198
if tt.expectKcp {
199-
clusterFromCtx, ok := result.Context().Value(targetcluster.LogicalClusterKey{}).(logicalcluster.Name)
199+
clusterFromCtx, ok := result.Context().Value(targetcluster.LogicalClusterKey("cluster")).(logicalcluster.Name)
200200
if !ok || clusterFromCtx != logicalcluster.Name(tt.workspace) {
201201
t.Errorf("expected cluster %q in context, got %q", tt.workspace, clusterFromCtx)
202202
}

0 commit comments

Comments
 (0)