Skip to content

Commit ba300a3

Browse files
committed
feat: added dots support for labels, annotations, nodeSelectors and matchLabels
On-behalf-of: @SAP [email protected] Signed-off-by: Artem Shcherbatiuk <[email protected]>
1 parent abca747 commit ba300a3

File tree

11 files changed

+1107
-44
lines changed

11 files changed

+1107
-44
lines changed

docs/dotted-keys.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Dotted Keys Support
2+
3+
GraphQL doesn't support dots in field names, but Kubernetes uses dotted keys extensively (e.g., `app.kubernetes.io/name`). This document explains how to work with such fields.
4+
5+
## Supported Fields
6+
7+
The following fields support dotted keys using a `Label` array format:
8+
9+
- `metadata.labels`
10+
- `metadata.annotations`
11+
- `spec.nodeSelector`
12+
- `spec.selector.matchLabels`
13+
- `spec.template.metadata.labels` (in Deployments)
14+
15+
## Querying
16+
17+
Use `key` and `value` sub-fields to access dotted keys:
18+
19+
```graphql
20+
query {
21+
core {
22+
Pod(namespace: "default", name: "my-pod") {
23+
metadata {
24+
labels {
25+
key
26+
value
27+
}
28+
annotations {
29+
key
30+
value
31+
}
32+
}
33+
}
34+
}
35+
}
36+
```
37+
38+
**Response:**
39+
```json
40+
{
41+
"data": {
42+
"core": {
43+
"Pod": {
44+
"metadata": {
45+
"labels": [
46+
{"key": "app.kubernetes.io/name", "value": "my-app"},
47+
{"key": "environment", "value": "production"}
48+
],
49+
"annotations": [
50+
{"key": "deployment.kubernetes.io/revision", "value": "1"}
51+
]
52+
}
53+
}
54+
}
55+
}
56+
}
57+
```
58+
59+
## Creating/Updating
60+
61+
Use array syntax with `key` and `value` objects:
62+
63+
```graphql
64+
mutation {
65+
apps {
66+
createDeployment(
67+
namespace: "default"
68+
object: {
69+
metadata: {
70+
name: "my-app"
71+
labels: [
72+
{key: "app.kubernetes.io/name", value: "my-app"},
73+
{key: "app.kubernetes.io/version", value: "1.0.0"}
74+
]
75+
annotations: [
76+
{key: "deployment.kubernetes.io/revision", value: "1"}
77+
]
78+
}
79+
spec: {
80+
selector: {
81+
matchLabels: [
82+
{key: "app.kubernetes.io/name", value: "my-app"}
83+
]
84+
}
85+
template: {
86+
spec: {
87+
nodeSelector: [
88+
{key: "kubernetes.io/arch", value: "amd64"}
89+
]
90+
}
91+
}
92+
}
93+
}
94+
) {
95+
metadata {
96+
name
97+
}
98+
}
99+
}
100+
}
101+
```
102+
103+
## Notes
104+
105+
- **No quotes** around `key` and `value` in GraphQL (they're field names, not strings)
106+
- Arrays are automatically converted to Kubernetes `map[string]string` format
107+
- Works with any keys containing dots or special characters
108+
- Supports all standard Kubernetes label/annotation patterns

docs/quickstart.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ You may checkout the following copy & paste examples to get started:
5959
- Subscribe to events using [Subscriptions](./subscriptions.md).
6060
- There are also [Custom Queries](./custom_queries.md) that go beyond what.
6161

62+
## Working with Dotted Keys (Labels, Annotations, NodeSelector, MatchLabels)
63+
64+
Kubernetes uses dotted keys extensively in various fields (e.g., `app.kubernetes.io/name`, `kubernetes.io/arch`), but GraphQL doesn't support dots in field names. Learn how to work with these fields using our special Label array format:
65+
- [Dotted Keys Guide](./dotted-keys.md) - Query and create `metadata.labels`, `metadata.annotations`, `spec.nodeSelector`, and `spec.selector.matchLabels` with dotted keys.
6266

6367
## Authorization with Remote Kuberenetes Clusters
6468

gateway/resolver/dotted_keys.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package resolver
2+
3+
// graphqlToKubernetes converts GraphQL input format to Kubernetes API format
4+
// []Label → map[string]string (for CREATE/UPDATE operations)
5+
func graphqlToKubernetes(obj any) any {
6+
objMap, ok := obj.(map[string]any)
7+
if !ok {
8+
return obj
9+
}
10+
11+
// Process metadata.labels and metadata.annotations
12+
if metadata := objMap["metadata"]; metadata != nil {
13+
objMap["metadata"] = processMetadataToMaps(metadata)
14+
}
15+
16+
// Process spec fields
17+
if spec := objMap["spec"]; spec != nil {
18+
objMap["spec"] = processSpecToMaps(spec)
19+
}
20+
21+
return obj
22+
}
23+
24+
// kubernetesToGraphQL converts Kubernetes API format to GraphQL output format
25+
// map[string]string → []Label (for QUERY operations)
26+
func kubernetesToGraphQL(obj any) any {
27+
objMap, ok := obj.(map[string]any)
28+
if !ok {
29+
return obj
30+
}
31+
32+
// Process metadata.labels and metadata.annotations
33+
if metadata := objMap["metadata"]; metadata != nil {
34+
objMap["metadata"] = processMetadataToArrays(metadata)
35+
}
36+
37+
// Process spec fields
38+
if spec := objMap["spec"]; spec != nil {
39+
objMap["spec"] = processSpecToArrays(spec)
40+
}
41+
42+
return obj
43+
}
44+
45+
// processMetadataToArrays handles metadata field conversion to arrays
46+
func processMetadataToArrays(metadata any) any {
47+
metadataMap, ok := metadata.(map[string]any)
48+
if !ok {
49+
return metadata
50+
}
51+
52+
for k, v := range metadataMap {
53+
if (k == "labels" || k == "annotations") && v != nil {
54+
metadataMap[k] = mapToArray(v)
55+
}
56+
}
57+
return metadata
58+
}
59+
60+
// processMetadataToMaps handles metadata field conversion to maps
61+
func processMetadataToMaps(metadata any) any {
62+
metadataMap, ok := metadata.(map[string]any)
63+
if !ok {
64+
return metadata
65+
}
66+
67+
for k, v := range metadataMap {
68+
if (k == "labels" || k == "annotations") && v != nil {
69+
metadataMap[k] = arrayToMap(v)
70+
}
71+
}
72+
return metadata
73+
}
74+
75+
// processSpecToArrays handles spec field conversion to arrays
76+
func processSpecToArrays(spec any) any {
77+
specMap, ok := spec.(map[string]any)
78+
if !ok {
79+
return spec
80+
}
81+
82+
for k, v := range specMap {
83+
if k == "nodeSelector" && v != nil {
84+
specMap[k] = mapToArray(v)
85+
} else if k == "selector" && v != nil {
86+
specMap[k] = processSelectorToArrays(v)
87+
} else if k == "template" && v != nil {
88+
specMap[k] = processTemplateToArrays(v)
89+
}
90+
}
91+
return spec
92+
}
93+
94+
// processSpecToMaps handles spec field conversion to maps
95+
func processSpecToMaps(spec any) any {
96+
specMap, ok := spec.(map[string]any)
97+
if !ok {
98+
return spec
99+
}
100+
101+
for k, v := range specMap {
102+
if k == "nodeSelector" && v != nil {
103+
specMap[k] = arrayToMap(v)
104+
} else if k == "selector" && v != nil {
105+
specMap[k] = processSelectorToMaps(v)
106+
} else if k == "template" && v != nil {
107+
specMap[k] = processTemplateToMaps(v)
108+
}
109+
}
110+
return spec
111+
}
112+
113+
// processSelectorToArrays handles spec.selector.matchLabels conversion to arrays
114+
func processSelectorToArrays(selector any) any {
115+
selectorMap, ok := selector.(map[string]any)
116+
if !ok {
117+
return selector
118+
}
119+
120+
for k, v := range selectorMap {
121+
if k == "matchLabels" && v != nil {
122+
selectorMap[k] = mapToArray(v)
123+
}
124+
}
125+
return selector
126+
}
127+
128+
// processSelectorToMaps handles spec.selector.matchLabels conversion to maps
129+
func processSelectorToMaps(selector any) any {
130+
selectorMap, ok := selector.(map[string]any)
131+
if !ok {
132+
return selector
133+
}
134+
135+
for k, v := range selectorMap {
136+
if k == "matchLabels" && v != nil {
137+
selectorMap[k] = arrayToMap(v)
138+
}
139+
}
140+
return selector
141+
}
142+
143+
// processTemplateToArrays handles spec.template.metadata and spec.template.spec conversion to arrays
144+
func processTemplateToArrays(template any) any {
145+
templateMap, ok := template.(map[string]any)
146+
if !ok {
147+
return template
148+
}
149+
150+
for k, v := range templateMap {
151+
if k == "metadata" && v != nil {
152+
templateMap[k] = processMetadataToArrays(v)
153+
} else if k == "spec" && v != nil {
154+
templateMap[k] = processSpecToArrays(v)
155+
}
156+
}
157+
return template
158+
}
159+
160+
// processTemplateToMaps handles spec.template.metadata and spec.template.spec conversion to maps
161+
func processTemplateToMaps(template any) any {
162+
templateMap, ok := template.(map[string]any)
163+
if !ok {
164+
return template
165+
}
166+
167+
for k, v := range templateMap {
168+
if k == "metadata" && v != nil {
169+
templateMap[k] = processMetadataToMaps(v)
170+
} else if k == "spec" && v != nil {
171+
templateMap[k] = processSpecToMaps(v)
172+
}
173+
}
174+
return template
175+
}
176+
177+
// mapToArray converts map[string]string to []Label
178+
func mapToArray(value any) any {
179+
valueMap, ok := value.(map[string]any)
180+
if !ok {
181+
return value
182+
}
183+
184+
labelArray := make([]map[string]any, 0, len(valueMap))
185+
for k, v := range valueMap {
186+
if strValue, ok := v.(string); ok {
187+
labelArray = append(labelArray, map[string]any{
188+
"key": k,
189+
"value": strValue,
190+
})
191+
}
192+
}
193+
return labelArray
194+
}
195+
196+
// arrayToMap converts []Label to map[string]string
197+
func arrayToMap(value any) any {
198+
valueArray, ok := value.([]any)
199+
if !ok {
200+
return value
201+
}
202+
203+
labelMap := make(map[string]string)
204+
for _, item := range valueArray {
205+
itemMap, ok := item.(map[string]any)
206+
if !ok {
207+
continue
208+
}
209+
210+
key, keyOk := itemMap["key"].(string)
211+
val, valOk := itemMap["value"].(string)
212+
if keyOk && valOk {
213+
labelMap[key] = val
214+
}
215+
}
216+
return labelMap
217+
}

0 commit comments

Comments
 (0)