diff --git a/docs/breaking-changes.md b/docs/breaking-changes.md index a2b0d44..13eddbc 100644 --- a/docs/breaking-changes.md +++ b/docs/breaking-changes.md @@ -8,7 +8,7 @@ GraphQL queries and mutations are now organized under `group → version → res - Core group examples: - Before: `core { ConfigMaps { ... } }` - - Now: `core { v1 { ConfigMaps { ... } } }` + - Now: `v1 { ConfigMaps { ... } }` - Non-core groups (dots replaced by underscores): - Before: `openmfp_org { Accounts { ... } }` @@ -18,14 +18,12 @@ Plural list fields now return a wrapper object with pagination metadata: ```graphql query { - core { - v1 { - ConfigMaps { - resourceVersion - continue - remainingItemCount - items { metadata { name namespace resourceVersion } } - } + v1 { + ConfigMaps { + resourceVersion + continue + remainingItemCount + items { metadata { name namespace resourceVersion } } } } } @@ -33,21 +31,21 @@ query { ## Subscriptions: flat and versioned field names -Subscriptions remain flat but now include the version in the field name: `__`. +Subscriptions remain flat and versioned. Core resources no longer include the artificial `core` group in the flattened name. -- Core examples: `core_v1_configmaps`, `core_v1_configmap` +- Core examples: `v1_configmaps`, `v1_configmap` - Non-core examples: `openmfp_org_v1alpha1_accounts`, `openmfp_org_v1alpha1_account` Subscriptions use Server‑Sent Events (SSE). GraphiQL/Playground typically do not support SSE subscriptions. Use curl/Postman/Insomnia or a custom EventSource client. -Example: +Example (core): ```sh curl \ -H "Accept: text/event-stream" \ -H "Content-Type: application/json" \ -d '{ - "query": "subscription { core_v1_configmaps { type object { metadata { name namespace resourceVersion } } } }" + "query": "subscription { v1_configmaps { type object { metadata { name namespace resourceVersion } } } }" }' \ $GRAPHQL_URL ``` @@ -58,6 +56,7 @@ To start from a known list `resourceVersion`, fetch it via a list query and pass - Legacy flat query/mutation names have been removed. - Old unversioned subscription names have been removed. +- Core flattened subscription names changed from `core_v1_*` to `v1_*`. Please update your client queries accordingly. See detailed examples: diff --git a/docs/configmap_queries.md b/docs/configmap_queries.md index f4aafe9..684073f 100644 --- a/docs/configmap_queries.md +++ b/docs/configmap_queries.md @@ -6,22 +6,20 @@ For questions on how to execute them, please find our [Quick Start Guide](./quic ## Create a ConfigMap: ```shell mutation { - core { - v1 { - createConfigMap( - namespace: "default", - object: { - metadata: { - name: "example-config" - }, - data: { key: "val" } - } - ) { - metadata { - name - } - data + v1 { + createConfigMap( + namespace: "default", + object: { + metadata: { + name: "example-config" + }, + data: { key: "val" } + } + ) { + metadata { + name } + data } } } @@ -30,20 +28,18 @@ mutation { ## List ConfigMaps: ```shell query { - core { - v1 { - ConfigMaps { - resourceVersion - continue - remainingItemCount - items { - metadata { - name - namespace - resourceVersion - } - data + v1 { + ConfigMaps { + resourceVersion + continue + remainingItemCount + items { + metadata { + name + namespace + resourceVersion } + data } } } @@ -53,14 +49,12 @@ query { ## Get a ConfigMap: ```shell { - core { - v1 { - ConfigMap(name: "example-config", namespace: "default") { - metadata { - name - } - data + v1 { + ConfigMap(name: "example-config", namespace: "default") { + metadata { + name } + data } } } @@ -69,21 +63,19 @@ query { ## Update a ConfigMap: ```shell mutation { - core { - v1 { - updateConfigMap( - name:"example-config" - namespace: "default", - object: { - data: { key: "new-value" } - } - ) { - metadata { - name - namespace - } - data + v1 { + updateConfigMap( + name:"example-config" + namespace: "default", + object: { + data: { key: "new-value" } + } + ) { + metadata { + name + namespace } + data } } } @@ -92,13 +84,11 @@ mutation { ## Delete a ConfigMap: ```shell mutation { - core { - v1 { - deleteConfigMap( - name: "example-config", - namespace: "default" - ) - } + v1 { + deleteConfigMap( + name: "example-config", + namespace: "default" + ) } } ``` diff --git a/docs/pod_queries.md b/docs/pod_queries.md index 4f1f398..1abd5b1 100644 --- a/docs/pod_queries.md +++ b/docs/pod_queries.md @@ -6,51 +6,49 @@ For questions on how to execute them, please find our [Quick Start Guide](./quic ## Create a Pod: ```shell mutation { - core { - v1 { - createPod( - namespace: "default", - object: { - metadata: { - name: "my-new-pod", - labels: { - app: "my-app" - } - } - spec: { - containers: [ - { - name: "nginx-container" - image: "nginx:latest" - ports: [ - { - containerPort: 80 - } - ] - } - ] - restartPolicy: "Always" + v1 { + createPod( + namespace: "default", + object: { + metadata: { + name: "my-new-pod", + labels: { + app: "my-app" } } - ) { - metadata { - name - namespace - labels - } - spec { - containers { - name - image - ports { - containerPort + spec: { + containers: [ + { + name: "nginx-container" + image: "nginx:latest" + ports: [ + { + containerPort: 80 + } + ] } - } - restartPolicy + ] + restartPolicy: "Always" } - status { - phase + } + ) { + metadata { + name + namespace + labels + } + spec { + containers { + name + image + ports { + containerPort + } } + restartPolicy + } + status { + phase } } } @@ -60,18 +58,16 @@ mutation { ## Get the Created Pod: ```shell query { - core { - v1 { - Pod(name:"my-new-pod", namespace:"default") { - metadata { - name - } - spec{ - containers { - image - ports { - containerPort - } + v1 { + Pod(name:"my-new-pod", namespace:"default") { + metadata { + name + } + spec{ + containers { + image + ports { + containerPort } } } @@ -83,13 +79,11 @@ query { ## Delete the Created Pod: ```shell mutation { - core { - v1 { - deletePod( - namespace: "default", - name: "my-new-pod" - ) - } + v1 { + deletePod( + namespace: "default", + name: "my-new-pod" + ) } } ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index f4305d9..d1ae54b 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -74,30 +74,28 @@ Kubernetes extensively uses dotted keys (e.g., `app.kubernetes.io/name`) in labe **Quick Example:** ```shell mutation createPodWithLabels($labels: StringMapInput) { - core { - v1 { - createPod( - namespace: "default" - object: { - metadata: { - name: "my-pod" - labels: $labels - } - spec: { - containers: [ - { - name: "nginx" - image: "nginx:latest" - ports: [{ containerPort: 80 }] - } - ] - } + v1 { + createPod( + namespace: "default" + object: { + metadata: { + name: "my-pod" + labels: $labels } - ) { - metadata { - labels # Returns: {"app.kubernetes.io/name": "my-app"} + spec: { + containers: [ + { + name: "nginx" + image: "nginx:latest" + ports: [{ containerPort: 80 }] + } + ] } } + ) { + metadata { + labels # Returns: {"app.kubernetes.io/name": "my-app"} + } } } } diff --git a/docs/subscriptions.md b/docs/subscriptions.md index f5886d8..ebb2668 100644 --- a/docs/subscriptions.md +++ b/docs/subscriptions.md @@ -18,20 +18,18 @@ Otherwise, only fields defined within the `{}` brackets will be listened to. ## Naming scheme (subscriptions) -Subscriptions are flat but versioned: `__`. +Subscriptions are flat but versioned. To align with the removal of the artificial core group: -- Core group examples: - - All ConfigMaps (core/v1): `core_v1_configmaps` - - Single ConfigMap: `core_v1_configmap` -- Non-core group examples (dots replaced with underscores): +- Core resources use `v_`. + - All ConfigMaps (core/v1): `v1_configmaps` + - Single ConfigMap: `v1_configmap` +- Non‑core (CRDs) remain `__` (dots in group replaced with underscores): - Accounts (openmfp.org/v1alpha1): `openmfp_org_v1alpha1_accounts` and `openmfp_org_v1alpha1_account` -Group and version match the hierarchical query/mutation path: +Group and version match the hierarchical query/mutation path. For example, the corresponding list query is: ``` query { - core { - v1 { ConfigMaps { resourceVersion items { metadata { name } } } } - } + v1 { ConfigMaps { resourceVersion items { metadata { name } } } } } ``` @@ -67,7 +65,7 @@ curl \ -H "Accept: text/event-stream" \ -H "Content-Type: application/json" \ -H "Authorization: $AUTHORIZATION_TOKEN" \ - -d '{"query": "subscription { core_v1_configmaps { type object { metadata { name resourceVersion } data } } }"}' \ + -d '{"query": "subscription { v1_configmaps { type object { metadata { name resourceVersion } data } } }"}' \ $GRAPHQL_URL ``` ### Subscribe to a Change of a Data Field in a Specific ConfigMap @@ -77,7 +75,7 @@ curl \ -H "Accept: text/event-stream" \ -H "Content-Type: application/json" \ -H "Authorization: $AUTHORIZATION_TOKEN" \ - -d '{"query": "subscription { core_v1_configmap(name: \"example-config\", namespace: \"default\") { type object { metadata { name resourceVersion } data } } }"}' \ + -d '{"query": "subscription { v1_configmap(name: \"example-config\", namespace: \"default\") { type object { metadata { name resourceVersion } data } } }"}' \ $GRAPHQL_URL ``` @@ -88,7 +86,7 @@ curl \ -H "Accept: text/event-stream" \ -H "Content-Type: application/json" \ -H "Authorization: $AUTHORIZATION_TOKEN" \ - -d '{"query": "subscription { core_v1_configmap(name: \"example-config\", namespace: \"default\", subscribeToAll: true) { type object { metadata { name resourceVersion } } } }"}' \ + -d '{"query": "subscription { v1_configmap(name: \"example-config\", namespace: \"default\", subscribeToAll: true) { type object { metadata { name resourceVersion } } } }"}' \ $GRAPHQL_URL ``` @@ -137,7 +135,7 @@ curl \ -H "Accept: text/event-stream" \ -H "Content-Type: application/json" \ -H "Authorization: $AUTHORIZATION_TOKEN" \ - -d '{"query": "subscription ($rv: String) { core_v1_configmaps(resourceVersion: $rv) { type object { metadata { name resourceVersion } } } }", "variables": {"rv":"12345"}}' \ + -d '{"query": "subscription ($rv: String) { v1_configmaps(resourceVersion: $rv) { type object { metadata { name resourceVersion } } } }", "variables": {"rv":"12345"}}' \ $GRAPHQL_URL ``` @@ -157,7 +155,7 @@ curl \ -H "Content-Type: application/json" \ -H "Authorization: $AUTHORIZATION_TOKEN" \ -d '{ - "query": "query { core { v1 { ConfigMaps { resourceVersion continue remainingItemCount items { metadata { name resourceVersion } data } } } } }" + "query": "query { v1 { ConfigMaps { resourceVersion continue remainingItemCount items { metadata { name resourceVersion } data } } } }" }' \ $GRAPHQL_URL ``` @@ -178,7 +176,7 @@ curl \ -H "Content-Type: application/json" \ -H "Authorization: $AUTHORIZATION_TOKEN" \ -d '{ - "query": "query { core { v1 { ConfigMaps(limit: 10) { continue items { metadata { name } } } } } }" + "query": "query { v1 { ConfigMaps(limit: 10) { continue items { metadata { name } } } } }" }' \ $GRAPHQL_URL ``` @@ -190,7 +188,7 @@ curl \ -H "Content-Type: application/json" \ -H "Authorization: $AUTHORIZATION_TOKEN" \ -d '{ - "query": "query($tok: String) { core { v1 { ConfigMaps(limit: 10, continue: $tok) { continue remainingItemCount items { metadata { name } } } } } }", + "query": "query($tok: String) { v1 { ConfigMaps(limit: 10, continue: $tok) { continue remainingItemCount items { metadata { name } } } } }", "variables": {"tok": ""} }' \ $GRAPHQL_URL diff --git a/gateway/schema/schema.go b/gateway/schema/schema.go index 3502141..8dfdeaa 100644 --- a/gateway/schema/schema.go +++ b/gateway/schema/schema.go @@ -90,15 +90,15 @@ func (g *Gateway) generateGraphqlSchema() error { newSchema, err := graphql.NewSchema(graphql.SchemaConfig{ Query: graphql.NewObject(graphql.ObjectConfig{ - Name: "PrivateNameForQuery", + Name: "Query", Fields: rootQueryFields, }), Mutation: graphql.NewObject(graphql.ObjectConfig{ - Name: "PrivateNameForMutation", + Name: "Mutation", Fields: rootMutationFields, }), Subscription: graphql.NewObject(graphql.ObjectConfig{ - Name: "PrivateNameForSubscription", + Name: "Subscription", Fields: rootSubscriptionFields, }), }) @@ -214,7 +214,6 @@ func (g *Gateway) processGroupedResources( versions[gvk.Version][resourceKey] = resourceScheme } - // For each version, create a nested object under the group for versionStr, resources := range versions { // Version objects queryVersionType := graphql.NewObject(graphql.ObjectConfig{ @@ -237,32 +236,50 @@ func (g *Gateway) processGroupedResources( ) } - // Attach version objects under the group only if they have fields - if len(queryVersionType.Fields()) > 0 { - queryGroupType.AddFieldConfig(versionStr, &graphql.Field{ - Type: queryVersionType, - Resolve: g.resolver.CommonResolver(), - }) - } - if len(mutationVersionType.Fields()) > 0 { - mutationGroupType.AddFieldConfig(versionStr, &graphql.Field{ - Type: mutationVersionType, - Resolve: g.resolver.CommonResolver(), - }) + // Attach version objects + if group == "core" { + // Target: expose core versions (e.g., v1) directly at root + if len(queryVersionType.Fields()) > 0 { + rootQueryFields[versionStr] = &graphql.Field{ + Type: queryVersionType, + Resolve: g.resolver.CommonResolver(), + } + } + if len(mutationVersionType.Fields()) > 0 { + rootMutationFields[versionStr] = &graphql.Field{ + Type: mutationVersionType, + Resolve: g.resolver.CommonResolver(), + } + } + } else { + // Default: attach version under the API group + if len(queryVersionType.Fields()) > 0 { + queryGroupType.AddFieldConfig(versionStr, &graphql.Field{ + Type: queryVersionType, + Resolve: g.resolver.CommonResolver(), + }) + } + if len(mutationVersionType.Fields()) > 0 { + mutationGroupType.AddFieldConfig(versionStr, &graphql.Field{ + Type: mutationVersionType, + Resolve: g.resolver.CommonResolver(), + }) + } } } - // Attach group objects at root - if len(queryGroupType.Fields()) > 0 { - rootQueryFields[group] = &graphql.Field{ - Type: queryGroupType, - Resolve: g.resolver.CommonResolver(), + if group != "core" { + if len(queryGroupType.Fields()) > 0 { + rootQueryFields[group] = &graphql.Field{ + Type: queryGroupType, + Resolve: g.resolver.CommonResolver(), + } } - } - if len(mutationGroupType.Fields()) > 0 { - rootMutationFields[group] = &graphql.Field{ - Type: mutationGroupType, - Resolve: g.resolver.CommonResolver(), + if len(mutationGroupType.Fields()) > 0 { + rootMutationFields[group] = &graphql.Field{ + Type: mutationGroupType, + Resolve: g.resolver.CommonResolver(), + } } } } @@ -395,8 +412,13 @@ func (g *Gateway) processSingleResource( }, }) - // Subscription field names are flat but versioned: __ - subscriptionSingular := strings.ToLower(fmt.Sprintf("%s_%s_%s", gvk.Group, gvk.Version, singular)) + // Subscription field names are flat but versioned. + var subscriptionSingular string + if gvk.Group == "core" { + subscriptionSingular = strings.ToLower(fmt.Sprintf("%s_%s", gvk.Version, singular)) + } else { + subscriptionSingular = strings.ToLower(fmt.Sprintf("%s_%s_%s", gvk.Group, gvk.Version, singular)) + } rootSubscriptionFields[subscriptionSingular] = &graphql.Field{ Type: eventType, Args: itemArgsBuilder. @@ -408,7 +430,12 @@ func (g *Gateway) processSingleResource( Description: fmt.Sprintf("Subscribe to changes of %s", singular), } - subscriptionPlural := strings.ToLower(fmt.Sprintf("%s_%s_%s", gvk.Group, gvk.Version, plural)) + var subscriptionPlural string + if gvk.Group == "core" { + subscriptionPlural = strings.ToLower(fmt.Sprintf("%s_%s", gvk.Version, plural)) + } else { + subscriptionPlural = strings.ToLower(fmt.Sprintf("%s_%s_%s", gvk.Group, gvk.Version, plural)) + } rootSubscriptionFields[subscriptionPlural] = &graphql.Field{ Type: eventType, Args: listArgsBuilder. diff --git a/tests/standard_k8s/gateway_test.go b/tests/standard_k8s/gateway_test.go index 0c0606a..02ababc 100644 --- a/tests/standard_k8s/gateway_test.go +++ b/tests/standard_k8s/gateway_test.go @@ -18,21 +18,19 @@ func (s *IntegrationTestSuite) TestConfigMapCRUD() { s.Run("List ConfigMaps", func() { result := s.executeGraphQL(clusterName, GraphQLRequest{ Query: ` - query { - core { - v1 { - ConfigMaps { - items { - metadata { - name - namespace - } - } - } - } - } - } - `, + query { + v1 { + ConfigMaps { + items { + metadata { + name + namespace + } + } + } + } + } + `, }) s.Require().Empty(result.Errors) @@ -42,20 +40,18 @@ func (s *IntegrationTestSuite) TestConfigMapCRUD() { s.Run("Create ConfigMap", func() { result := s.executeGraphQL(clusterName, GraphQLRequest{ Query: ` - mutation CreateConfigMap($namespace: String!, $object: ConfigMapInput!) { - core { - v1 { - createConfigMap(namespace: $namespace, object: $object) { - metadata { - name - namespace - } - data - } - } - } - } - `, + mutation CreateConfigMap($namespace: String!, $object: ConfigMapInput!) { + v1 { + createConfigMap(namespace: $namespace, object: $object) { + metadata { + name + namespace + } + data + } + } + } + `, Variables: map[string]any{ "namespace": "default", "object": map[string]any{ @@ -94,21 +90,19 @@ func (s *IntegrationTestSuite) TestConfigMapCRUD() { s.Run("Get ConfigMap", func() { result := s.executeGraphQL(clusterName, GraphQLRequest{ Query: ` - query GetConfigMap($name: String!, $namespace: String!) { - core { - v1 { - ConfigMap(name: $name, namespace: $namespace) { - metadata { - name - namespace - labels - } - data - } - } - } - } - `, + query GetConfigMap($name: String!, $namespace: String!) { + v1 { + ConfigMap(name: $name, namespace: $namespace) { + metadata { + name + namespace + labels + } + data + } + } + } + `, Variables: map[string]any{ "name": configMapName, "namespace": "default", @@ -122,21 +116,19 @@ func (s *IntegrationTestSuite) TestConfigMapCRUD() { s.Run("Query by label selector", func() { result := s.executeGraphQL(clusterName, GraphQLRequest{ Query: ` - query ListConfigMaps($namespace: String!, $labelselector: String!) { - core { - v1 { - ConfigMaps(namespace: $namespace, labelselector: $labelselector) { - items { - metadata { - name - labels - } - } - } - } - } - } - `, + query ListConfigMaps($namespace: String!, $labelselector: String!) { + v1 { + ConfigMaps(namespace: $namespace, labelselector: $labelselector) { + items { + metadata { + name + labels + } + } + } + } + } + `, Variables: map[string]any{ "namespace": "default", "labelselector": "app=myapp", @@ -150,20 +142,18 @@ func (s *IntegrationTestSuite) TestConfigMapCRUD() { s.Run("Update ConfigMap", func() { result := s.executeGraphQL(clusterName, GraphQLRequest{ Query: ` - mutation UpdateConfigMap($name: String!, $namespace: String!, $object: ConfigMapInput!) { - core { - v1 { - updateConfigMap(name: $name, namespace: $namespace, object: $object) { - metadata { - name - namespace - } - data - } - } - } - } - `, + mutation UpdateConfigMap($name: String!, $namespace: String!, $object: ConfigMapInput!) { + v1 { + updateConfigMap(name: $name, namespace: $namespace, object: $object) { + metadata { + name + namespace + } + data + } + } + } + `, Variables: map[string]any{ "name": configMapName, "namespace": "default", @@ -203,14 +193,12 @@ func (s *IntegrationTestSuite) TestConfigMapCRUD() { s.Run("Delete ConfigMap", func() { result := s.executeGraphQL(clusterName, GraphQLRequest{ Query: ` - mutation DeleteConfigMap($name: String!, $namespace: String!) { - core { - v1 { - deleteConfigMap(name: $name, namespace: $namespace) - } - } - } - `, + mutation DeleteConfigMap($name: String!, $namespace: String!) { + v1 { + deleteConfigMap(name: $name, namespace: $namespace) + } + } + `, Variables: map[string]any{ "name": configMapName, "namespace": "default",