diff --git a/common/auth/metadata_injector.go b/common/auth/metadata_injector.go index 7af6e2d..f2d4725 100644 --- a/common/auth/metadata_injector.go +++ b/common/auth/metadata_injector.go @@ -22,7 +22,6 @@ import ( // MetadataInjectionConfig contains configuration for metadata injection type MetadataInjectionConfig struct { Host string - Path string Auth *gatewayv1alpha1.AuthConfig CA *gatewayv1alpha1.CAConfig HostOverride string // For virtual workspaces @@ -57,7 +56,6 @@ func (m *MetadataInjector) InjectClusterMetadata(ctx context.Context, schemaJSON // Create cluster metadata metadata := map[string]any{ "host": host, - "path": config.Path, } // Add auth data if configured @@ -84,7 +82,7 @@ func (m *MetadataInjector) InjectClusterMetadata(ctx context.Context, schemaJSON m.tryExtractKubeconfigCA(ctx, config.Auth, metadata) } - return m.finalizeSchemaInjection(schemaData, metadata, host, config.Path, config.CA != nil || config.Auth != nil) + return m.finalizeSchemaInjection(schemaData, metadata, host, config.CA != nil || config.Auth != nil) } // InjectKCPMetadataFromEnv injects KCP metadata using kubeconfig from environment @@ -114,7 +112,6 @@ func (m *MetadataInjector) InjectKCPMetadataFromEnv(schemaJSON []byte, clusterPa // Create cluster metadata with environment kubeconfig metadata := map[string]any{ "host": host, - "path": clusterPath, "auth": map[string]any{ "type": "kubeconfig", "kubeconfig": base64.StdEncoding.EncodeToString(kubeconfigData), @@ -129,7 +126,7 @@ func (m *MetadataInjector) InjectKCPMetadataFromEnv(schemaJSON []byte, clusterPa } } - return m.finalizeSchemaInjection(schemaData, metadata, host, clusterPath, caData != nil) + return m.finalizeSchemaInjection(schemaData, metadata, host, caData != nil) } // extractAuthDataForMetadata extracts auth data from AuthConfig for metadata injection @@ -436,7 +433,7 @@ func (m *MetadataInjector) determineKCPHost(kubeconfigHost, override, clusterPat } // finalizeSchemaInjection finalizes the schema injection process -func (m *MetadataInjector) finalizeSchemaInjection(schemaData map[string]any, metadata map[string]any, host, path string, hasCA bool) ([]byte, error) { +func (m *MetadataInjector) finalizeSchemaInjection(schemaData map[string]any, metadata map[string]any, host string, hasCA bool) ([]byte, error) { // Inject the metadata into the schema schemaData["x-cluster-metadata"] = metadata @@ -448,7 +445,6 @@ func (m *MetadataInjector) finalizeSchemaInjection(schemaData map[string]any, me m.log.Info(). Str("host", host). - Str("path", path). Bool("hasCA", hasCA). Msg("successfully injected cluster metadata into schema") diff --git a/common/auth/metadata_injector_test.go b/common/auth/metadata_injector_test.go index aae4472..5a8827d 100644 --- a/common/auth/metadata_injector_test.go +++ b/common/auth/metadata_injector_test.go @@ -157,11 +157,6 @@ users: require.True(t, exists, "host should be present") assert.Equal(t, tt.expectedHost, host) - // Verify path - path, exists := metadataMap["path"] - require.True(t, exists, "path should be present") - assert.Equal(t, tt.clusterPath, path) - // Verify auth auth, exists := metadataMap["auth"] require.True(t, exists, "auth should be present") @@ -198,7 +193,6 @@ func TestInjectClusterMetadata(t *testing.T) { schemaJSON []byte config MetadataInjectionConfig expectedHost string - expectedPath string expectError bool }{ { @@ -217,10 +211,8 @@ func TestInjectClusterMetadata(t *testing.T) { }`), config: MetadataInjectionConfig{ Host: "https://test-cluster.example.com:6443", - Path: "test-cluster", }, expectedHost: "https://test-cluster.example.com:6443", - expectedPath: "test-cluster", expectError: false, }, { @@ -234,11 +226,9 @@ func TestInjectClusterMetadata(t *testing.T) { }`), config: MetadataInjectionConfig{ Host: "https://original.example.com:6443", - Path: "virtual-workspace/test", HostOverride: "https://override.example.com:6443/services/test", }, expectedHost: "https://override.example.com:6443/services/test", - expectedPath: "virtual-workspace/test", expectError: false, }, { @@ -252,10 +242,8 @@ func TestInjectClusterMetadata(t *testing.T) { }`), config: MetadataInjectionConfig{ Host: "https://kcp.example.com:6443/services/apiexport/some/path", - Path: "test-workspace", }, expectedHost: "https://kcp.example.com:6443", // Should be stripped - expectedPath: "test-workspace", expectError: false, }, { @@ -267,7 +255,6 @@ func TestInjectClusterMetadata(t *testing.T) { }`), config: MetadataInjectionConfig{ Host: "https://test.example.com:6443", - Path: "test", }, expectError: true, }, @@ -302,11 +289,6 @@ func TestInjectClusterMetadata(t *testing.T) { host, exists := metadataMap["host"] require.True(t, exists, "host should be present") assert.Equal(t, tt.expectedHost, host) - - // Verify path - path, exists := metadataMap["path"] - require.True(t, exists, "path should be present") - assert.Equal(t, tt.expectedPath, path) }) } } diff --git a/docs/clusteraccess.md b/docs/clusteraccess.md index c68d5a6..9832676 100644 --- a/docs/clusteraccess.md +++ b/docs/clusteraccess.md @@ -33,7 +33,9 @@ kind: ClusterAccess metadata: name: my-target-cluster spec: - path: my-target-cluster # Used as schema filename + # Optional: path overrides the output schema filename. If omitted, metadata.name is used. + # Note: This value is not used by the gateway at runtime. + path: my-target-cluster host: https://my-cluster-api-server:6443 auth: kubeconfigSecretRef: @@ -68,14 +70,13 @@ The listener: Generated schema files contain: -```json +``` { "definitions": { - // ... Kubernetes API definitions + }, "x-cluster-metadata": { "host": "https://my-cluster-api-server:6443", - "path": "my-target-cluster", "auth": { "type": "kubeconfig", "kubeconfig": "base64-encoded-kubeconfig" @@ -87,6 +88,7 @@ Generated schema files contain: } ``` + ### 4. Gateway Usage When running the gateway in ClusterAccess mode: @@ -99,12 +101,13 @@ export GATEWAY_SHOULD_IMPERSONATE=false The gateway: - Watches the definitions directory for schema files - For each schema file: - - Reads the `x-cluster-metadata` section + - Reads the `x-cluster-metadata` section (only `host`, `auth`, `ca` are used) - Creates a `rest.Config` using the embedded connection info - Establishes a Kubernetes client connection to the target cluster - Serves GraphQL API at `/{cluster-name}/graphql` - **Does NOT require KUBECONFIG** - all connection info comes from schema files + ## Troubleshooting ### Schema files not generated @@ -120,4 +123,4 @@ The gateway: ### Connection errors - Verify target cluster URLs are accessible - Check CA certificates are correct -- Validate authentication credentials have required permissions \ No newline at end of file +- Validate authentication credentials have required permissions diff --git a/docs/listener.md b/docs/listener.md index 761bd0f..f3eb090 100644 --- a/docs/listener.md +++ b/docs/listener.md @@ -19,6 +19,45 @@ Contains reconciliation logic for different operational modes: - Generates schema files with embedded cluster connection metadata - Injects `x-cluster-metadata` into schema files for gateway consumption +## Custom OpenAPI properties used by the project + +The project uses two categories of OpenAPI extensions: + +- Kubernetes-standard extensions (consumed during schema building): + - `x-kubernetes-group-version-kind` + - `x-kubernetes-categories` + - `x-kubernetes-scope` + These are provided by kube-openapi and are not defined by this project. + +- Project-defined extension (produced by the listener and consumed by the gateway): + - `x-cluster-metadata`: carries connection information for the target cluster. + +### `x-cluster-metadata` + +The listener injects `x-cluster-metadata` into each schema file so the gateway can establish a connection to the referenced cluster without any external configuration. + +Structure (minimal shape used by the gateway): + +``` +"x-cluster-metadata": { + "host": "https://", + "auth": { /* one of: token | kubeconfig | clientCert */ }, + "ca": { "data": "" } +} +``` + +Notes: +- The `host` field is required. +- Exactly one authentication method should be provided under `auth`: + - `{"type":"token","token":""}` + - `{"type":"kubeconfig","kubeconfig":""}` + - `{"type":"clientCert","certData":"","keyData":""}` +- The `ca.data` field is optional but recommended; if omitted and `auth` contains a kubeconfig, the listener attempts to extract the CA from that kubeconfig automatically. + +Why we keep it simple: +- All information needed to connect is either intrinsic to the target cluster (host, CA) or already available via selected auth material. We avoid injecting duplicate or derivable data. +- We do not replicate routing information in metadata; routing is defined by where the file is stored and how the gateway is addressed. + #### KCP Reconciler (`reconciler/kcp/`) - Watches APIBinding resources in KCP workspaces - Discovers virtual workspaces and their API resources diff --git a/gateway/manager/targetcluster/cluster.go b/gateway/manager/targetcluster/cluster.go index a2b1840..12a01e2 100644 --- a/gateway/manager/targetcluster/cluster.go +++ b/gateway/manager/targetcluster/cluster.go @@ -29,7 +29,6 @@ type FileData struct { // ClusterMetadata represents the cluster connection metadata stored in schema files type ClusterMetadata struct { Host string `json:"host"` - Path string `json:"path,omitempty"` Auth *AuthMetadata `json:"auth,omitempty"` CA *CAMetadata `json:"ca,omitempty"` } diff --git a/listener/reconciler/clusteraccess/metadata_injector.go b/listener/reconciler/clusteraccess/metadata_injector.go index b3fd91b..1d000d0 100644 --- a/listener/reconciler/clusteraccess/metadata_injector.go +++ b/listener/reconciler/clusteraccess/metadata_injector.go @@ -11,16 +11,9 @@ import ( ) func injectClusterMetadata(ctx context.Context, schemaJSON []byte, clusterAccess gatewayv1alpha1.ClusterAccess, k8sClient client.Client, log *logger.Logger) ([]byte, error) { - // Determine the path - path := clusterAccess.Spec.Path - if path == "" { - path = clusterAccess.GetName() - } - // Create metadata injection config config := auth.MetadataInjectionConfig{ Host: clusterAccess.Spec.Host, - Path: path, Auth: clusterAccess.Spec.Auth, CA: clusterAccess.Spec.CA, } diff --git a/listener/reconciler/clusteraccess/metadata_injector_test.go b/listener/reconciler/clusteraccess/metadata_injector_test.go index 4047e2f..9ce68e2 100644 --- a/listener/reconciler/clusteraccess/metadata_injector_test.go +++ b/listener/reconciler/clusteraccess/metadata_injector_test.go @@ -38,7 +38,6 @@ func TestInjectClusterMetadata(t *testing.T) { mockSetup: func(m *mocks.MockClient) {}, wantMetadata: map[string]any{ "host": "https://test-cluster.example.com", - "path": "test-cluster", }, wantErr: false, }, @@ -55,7 +54,6 @@ func TestInjectClusterMetadata(t *testing.T) { mockSetup: func(m *mocks.MockClient) {}, wantMetadata: map[string]any{ "host": "https://test-cluster.example.com", - "path": "custom-path", }, wantErr: false, }, @@ -83,7 +81,6 @@ func TestInjectClusterMetadata(t *testing.T) { mockSetup: func(m *mocks.MockClient) {}, wantMetadata: map[string]any{ "host": "https://example.com", - "path": "", }, wantErr: false, }, @@ -100,7 +97,6 @@ func TestInjectClusterMetadata(t *testing.T) { mockSetup: func(m *mocks.MockClient) {}, wantMetadata: map[string]any{ "host": "https://example.com", - "path": "", }, wantErr: false, }, @@ -116,7 +112,6 @@ func TestInjectClusterMetadata(t *testing.T) { mockSetup: func(m *mocks.MockClient) {}, wantMetadata: map[string]any{ "host": "", - "path": "no-host-cluster", }, wantErr: false, }, @@ -133,7 +128,6 @@ func TestInjectClusterMetadata(t *testing.T) { mockSetup: func(m *mocks.MockClient) {}, wantMetadata: map[string]any{ "host": "https://special.example.com", - "path": "special/chars_path.test", }, wantErr: false, }, @@ -149,7 +143,6 @@ func TestInjectClusterMetadata(t *testing.T) { mockSetup: func(m *mocks.MockClient) {}, wantMetadata: map[string]any{ "host": "https://minimal.example.com", - "path": "minimal", }, wantErr: false, }, @@ -166,7 +159,6 @@ func TestInjectClusterMetadata(t *testing.T) { mockSetup: func(m *mocks.MockClient) {}, wantMetadata: map[string]any{ "host": "https://example.com", - "path": "very/long/path/with/multiple/segments", }, wantErr: false, }, @@ -182,7 +174,6 @@ func TestInjectClusterMetadata(t *testing.T) { mockSetup: func(m *mocks.MockClient) {}, wantMetadata: map[string]any{ "host": "https://unicode.example.com", - "path": "üñíçødé-cluster", }, wantErr: false, }, @@ -251,50 +242,3 @@ func TestInjectClusterMetadata(t *testing.T) { }) } } - -func TestInjectClusterMetadata_PathLogic(t *testing.T) { - mockLogger, err := logger.New(logger.DefaultConfig()) - require.NoError(t, err) - mockClient := mocks.NewMockClient(t) - schemaJSON := []byte(`{"openapi": "3.0.0", "info": {"title": "Test"}}`) - - t.Run("path_precedence_custom_over_name", func(t *testing.T) { - clusterAccess := gatewayv1alpha1.ClusterAccess{ - ObjectMeta: metav1.ObjectMeta{Name: "cluster-name"}, - Spec: gatewayv1alpha1.ClusterAccessSpec{ - Host: "https://test.example.com", - Path: "custom-path", - }, - } - - result, err := clusteraccess.InjectClusterMetadata(t.Context(), schemaJSON, clusterAccess, mockClient, mockLogger) - require.NoError(t, err) - - var resultData map[string]any - err = json.Unmarshal(result, &resultData) - require.NoError(t, err) - - metadata := resultData["x-cluster-metadata"].(map[string]any) - assert.Equal(t, "custom-path", metadata["path"]) - }) - - t.Run("fallback_to_name_when_path_empty", func(t *testing.T) { - clusterAccess := gatewayv1alpha1.ClusterAccess{ - ObjectMeta: metav1.ObjectMeta{Name: "fallback-name"}, - Spec: gatewayv1alpha1.ClusterAccessSpec{ - Host: "https://test.example.com", - Path: "", - }, - } - - result, err := clusteraccess.InjectClusterMetadata(t.Context(), schemaJSON, clusterAccess, mockClient, mockLogger) - require.NoError(t, err) - - var resultData map[string]any - err = json.Unmarshal(result, &resultData) - require.NoError(t, err) - - metadata := resultData["x-cluster-metadata"].(map[string]any) - assert.Equal(t, "fallback-name", metadata["path"]) - }) -} diff --git a/listener/reconciler/kcp/apibinding_controller_test.go b/listener/reconciler/kcp/apibinding_controller_test.go index 208faa3..fb4daef 100644 --- a/listener/reconciler/kcp/apibinding_controller_test.go +++ b/listener/reconciler/kcp/apibinding_controller_test.go @@ -285,7 +285,6 @@ users: assert.Contains(t, s, `"schema":"test"`) assert.Contains(t, s, `"x-cluster-metadata"`) assert.Contains(t, s, `"host":"https://test.example.com"`) - assert.Contains(t, s, `"path":"root:org:new-cluster"`) }, wantResult: ctrl.Result{}, wantErr: false, @@ -434,7 +433,6 @@ users: s := string(data) assert.Contains(t, s, `"schema":"existing"`) assert.Contains(t, s, `"x-cluster-metadata"`) - assert.Contains(t, s, `"path":"root:org:unchanged-cluster"`) }, wantResult: ctrl.Result{}, wantErr: false, @@ -488,7 +486,6 @@ users: s := string(data) assert.Contains(t, s, `"schema":"new"`) assert.Contains(t, s, `"x-cluster-metadata"`) - assert.Contains(t, s, `"path":"root:org:changed-cluster"`) }, wantResult: ctrl.Result{}, wantErr: false,