|
7 | 7 | "slices"
|
8 | 8 | "strings"
|
9 | 9 |
|
| 10 | + "github.com/google/go-cmp/cmp" |
10 | 11 | "github.com/graphql-go/graphql"
|
11 | 12 | "github.com/mitchellh/mapstructure"
|
12 | 13 | "go.opentelemetry.io/otel"
|
@@ -283,9 +284,94 @@ func (r *resolver) updateItem(crd apiextensionsv1.CustomResourceDefinition, type
|
283 | 284 | }
|
284 | 285 | }
|
285 | 286 |
|
| 287 | +func (r *resolver) subscribeItem(crd apiextensionsv1.CustomResourceDefinition, typeInformation apiextensionsv1.CustomResourceDefinitionVersion) func(p graphql.ResolveParams) (interface{}, error) { |
| 288 | + logger := slog.With(slog.String("operation", "subribeItem"), slog.String("kind", crd.Spec.Names.Kind), slog.String("version", typeInformation.Name)) |
| 289 | + return func(p graphql.ResolveParams) (interface{}, error) { |
| 290 | + ctx, span := otel.Tracer("").Start(p.Context, "SubscribeForObject", trace.WithAttributes(attribute.String("kind", crd.Spec.Names.Kind))) |
| 291 | + defer span.End() |
| 292 | + |
| 293 | + var metadatInput MetadatInput |
| 294 | + if err := mapstructure.Decode(p.Args, &metadatInput); err != nil { |
| 295 | + logger.Error("unable to decode metadata input", "error", err) |
| 296 | + return nil, err |
| 297 | + } |
| 298 | + |
| 299 | + listType, ok := r.conf.pluralToListType[crd.Spec.Names.Plural] |
| 300 | + if !ok { |
| 301 | + logger.Error("no typed client available for the reuqested type") |
| 302 | + return nil, errors.New("no typed client available for the reuqested type") |
| 303 | + } |
| 304 | + |
| 305 | + if err := isAuthorized(ctx, r.conf.Client, authzv1.ResourceAttributes{ |
| 306 | + Verb: "watch", |
| 307 | + Group: crd.Spec.Group, |
| 308 | + Version: typeInformation.Name, |
| 309 | + Resource: crd.Spec.Names.Plural, |
| 310 | + Namespace: metadatInput.Namespace, |
| 311 | + Name: metadatInput.Name, |
| 312 | + }); err != nil { |
| 313 | + return nil, err |
| 314 | + } |
| 315 | + |
| 316 | + listWatch, err := r.conf.Client.Watch(ctx, listType(), client.InNamespace(metadatInput.Namespace), client.MatchingFields{"metadata.name": metadatInput.Name}) |
| 317 | + if err != nil { |
| 318 | + logger.Error("unable to watch object", slog.Any("error", err)) |
| 319 | + return nil, err |
| 320 | + } |
| 321 | + |
| 322 | + resultChannel := make(chan interface{}) |
| 323 | + |
| 324 | + go func() { |
| 325 | + var item client.Object |
| 326 | + for ev := range listWatch.ResultChan() { |
| 327 | + changed := false |
| 328 | + select { |
| 329 | + case <-ctx.Done(): |
| 330 | + slog.Info("stopping watch due to client cancel") |
| 331 | + listWatch.Stop() |
| 332 | + close(resultChannel) |
| 333 | + return |
| 334 | + default: |
| 335 | + switch ev.Type { |
| 336 | + case watch.Added: |
| 337 | + item = ev.Object.(client.Object) |
| 338 | + changed = true |
| 339 | + case watch.Modified: |
| 340 | + |
| 341 | + emitOnlyFieldChanges, ok := p.Args["emitOnlyFieldChanges"].(bool) |
| 342 | + |
| 343 | + changed = determineFieldChanged(ok && emitOnlyFieldChanges, ev.Object.(client.Object), resultChannel, p, item) |
| 344 | + item = ev.Object.(client.Object) |
| 345 | + case watch.Deleted: |
| 346 | + itemType, ok := r.conf.pluralToObjectType[crd.Spec.Names.Plural] |
| 347 | + if !ok { |
| 348 | + logger.Error("no typed client available for the reuqested type") |
| 349 | + listWatch.Stop() |
| 350 | + close(resultChannel) |
| 351 | + } |
| 352 | + |
| 353 | + item = itemType() |
| 354 | + default: |
| 355 | + logger.Info("skipping event", "event", ev.Type, "object", ev.Object) |
| 356 | + continue |
| 357 | + } |
| 358 | + } |
| 359 | + |
| 360 | + if val, ok := p.Args["emitOnlyFieldChanges"].(bool); ok && val && changed { |
| 361 | + resultChannel <- item |
| 362 | + } else if !ok || !val { |
| 363 | + resultChannel <- item |
| 364 | + } |
| 365 | + } |
| 366 | + }() |
| 367 | + |
| 368 | + return resultChannel, nil |
| 369 | + } |
| 370 | +} |
| 371 | + |
286 | 372 | func (r *resolver) subscribeItems(crd apiextensionsv1.CustomResourceDefinition, typeInformation apiextensionsv1.CustomResourceDefinitionVersion) func(p graphql.ResolveParams) (interface{}, error) {
|
287 | 373 | return func(p graphql.ResolveParams) (interface{}, error) {
|
288 |
| - ctx, span := otel.Tracer("").Start(p.Context, "Subscribe", trace.WithAttributes(attribute.String("kind", crd.Spec.Names.Kind))) |
| 374 | + ctx, span := otel.Tracer("").Start(p.Context, "SubscribeForNamespace", trace.WithAttributes(attribute.String("kind", crd.Spec.Names.Kind))) |
289 | 375 | defer span.End()
|
290 | 376 |
|
291 | 377 | listType, ok := r.conf.pluralToListType[crd.Spec.Names.Plural]
|
@@ -330,47 +416,9 @@ func (r *resolver) subscribeItems(crd apiextensionsv1.CustomResourceDefinition,
|
330 | 416 | for i, item := range items {
|
331 | 417 | if item.GetName() == ev.Object.(client.Object).GetName() {
|
332 | 418 |
|
333 |
| - if val, ok := p.Args["emitOnlyFieldChanges"].(bool); ok && val { |
334 |
| - unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(ev.Object) |
335 |
| - if err != nil { |
336 |
| - // TODO: handle error |
337 |
| - close(resultChannel) |
338 |
| - } |
339 |
| - |
340 |
| - fields := getRequestedFields(p) |
341 |
| - |
342 |
| - currentItemUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(item) |
343 |
| - if err != nil { |
344 |
| - // TODO: handle error |
345 |
| - close(resultChannel) |
346 |
| - } |
347 |
| - |
348 |
| - for _, field := range fields { |
349 |
| - fieldValue, found, err := unstructured.NestedFieldNoCopy(unstructuredObj, strings.Split(field, ".")...) |
350 |
| - if err != nil { |
351 |
| - // TODO: handle error |
352 |
| - slog.Error("unable to get field value", "error", err) |
353 |
| - close(resultChannel) |
354 |
| - } |
355 |
| - |
356 |
| - currentFieldValue, currentFound, err := unstructured.NestedFieldNoCopy(currentItemUnstructured, strings.Split(field, ".")...) |
357 |
| - if err != nil { |
358 |
| - // TODO: handle error |
359 |
| - slog.Error("unable to get field value", "error", err) |
360 |
| - close(resultChannel) |
361 |
| - } |
362 |
| - |
363 |
| - if !found || !currentFound { |
364 |
| - continue |
365 |
| - } |
366 |
| - if fieldValue == currentFieldValue { |
367 |
| - continue |
368 |
| - } |
369 |
| - |
370 |
| - changed = true |
371 |
| - |
372 |
| - } |
373 |
| - } |
| 419 | + emitOnlyFieldChanges, ok := p.Args["emitOnlyFieldChanges"].(bool) |
| 420 | + |
| 421 | + changed = determineFieldChanged(ok && emitOnlyFieldChanges, ev.Object.(client.Object), resultChannel, p, item) |
374 | 422 |
|
375 | 423 | items[i] = ev.Object.(client.Object)
|
376 | 424 | break
|
@@ -402,6 +450,53 @@ func (r *resolver) subscribeItems(crd apiextensionsv1.CustomResourceDefinition,
|
402 | 450 | }
|
403 | 451 | }
|
404 | 452 |
|
| 453 | +func determineFieldChanged(emitOnlyFieldChanges bool, obj client.Object, resultChannel chan interface{}, p graphql.ResolveParams, currentItem client.Object) bool { |
| 454 | + if !emitOnlyFieldChanges { |
| 455 | + return true |
| 456 | + } |
| 457 | + |
| 458 | + unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) |
| 459 | + if err != nil { |
| 460 | + // TODO: handle error |
| 461 | + close(resultChannel) |
| 462 | + } |
| 463 | + |
| 464 | + fields := getRequestedFields(p) |
| 465 | + |
| 466 | + currentItemUnstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(currentItem) |
| 467 | + if err != nil { |
| 468 | + // TODO: handle error |
| 469 | + close(resultChannel) |
| 470 | + } |
| 471 | + |
| 472 | + for _, field := range fields { |
| 473 | + fieldValue, found, err := unstructured.NestedFieldNoCopy(unstructuredObj, strings.Split(field, ".")...) |
| 474 | + if err != nil { |
| 475 | + // TODO: handle error |
| 476 | + slog.Error("unable to get field value", "error", err) |
| 477 | + close(resultChannel) |
| 478 | + } |
| 479 | + |
| 480 | + currentFieldValue, currentFound, err := unstructured.NestedFieldNoCopy(currentItemUnstructured, strings.Split(field, ".")...) |
| 481 | + if err != nil { |
| 482 | + // TODO: handle error |
| 483 | + slog.Error("unable to get field value", "error", err) |
| 484 | + close(resultChannel) |
| 485 | + } |
| 486 | + |
| 487 | + if !found || !currentFound { |
| 488 | + continue |
| 489 | + } |
| 490 | + if cmp.Equal(fieldValue, currentFieldValue) { |
| 491 | + continue |
| 492 | + } |
| 493 | + |
| 494 | + return true |
| 495 | + } |
| 496 | + |
| 497 | + return false |
| 498 | +} |
| 499 | + |
405 | 500 | func isAuthorized(ctx context.Context, c client.Client, resourceAttributes authzv1.ResourceAttributes) error {
|
406 | 501 | ctx, span := otel.Tracer("").Start(ctx, "AuthorizationCheck")
|
407 | 502 | defer span.End()
|
|
0 commit comments