Skip to content

Comments

Add more info to adapter framework task execution#51

Merged
openshift-merge-bot[bot] merged 1 commit intoopenshift-hyperfleet:mainfrom
rh-amarin:devx-enhancements
Feb 19, 2026
Merged

Add more info to adapter framework task execution#51
openshift-merge-bot[bot] merged 1 commit intoopenshift-hyperfleet:mainfrom
rh-amarin:devx-enhancements

Conversation

@rh-amarin
Copy link
Contributor

@rh-amarin rh-amarin commented Feb 19, 2026

https://issues.redhat.com/browse/HYPERFLEET-653

Improvements to adapter framework

  • Custom CEL functions (toJson, dig): Add helper functions to CEL expressions for safely traversing deeply nested resource structures and serializing objects to JSON
    • toJson: converts object to JSON string, useful for debugging adapters
    • dig: fetches a nested object with dot notation of returns false
  • Nested discovery stored as top-level context entries:
    • Previous functionality only extracted parts of the original manifest for nested discovered objects
    • Now it extracts data from maestro response, adding:
      • manifest : the previous functionality
      • statusFeedback : to look for remote object status
      • conditions: maestro conditions on the resources
  • Discovery failure is now a hard error: Post-apply discovery failures now set ExecutionError, mark the result as StatusFailed, and abort execution instead of silently logging a warning
    • Before this PR, errors where swallowed silently
  • Dry-run label selector filtering: The dry-run transport client now filters discovered resources by label selector when applicable
  • Debug logging for API call payloads: POST/PUT/PATCH API calls now log the rendered payload at debug level for easier troubleshooting
    • We may need to revisit this decision if it leads to too much detail in the logs (and cost $$)
  • Updated and expanded test data for both Kubernetes and Maestro adapter dry-run scenarios
  • Added a dry-run example showcasing all CEL expression possibilities

Summary by CodeRabbit

  • New Features

    • Added CEL helpers for JSON serialization and safe nested data traversal
    • Enabled label-selector filtering during resource discovery
  • Improvements

    • Nested discoveries are now surfaced as separate top-level resources and inherit status/conditions from their parent when available
    • API request payloads are logged for easier debugging
  • Tests

    • Expanded dry-run fixtures and tests covering CEL helpers, payload building, discovery scenarios, and error handling

@openshift-ci openshift-ci bot requested review from Mischulee and crizzo71 February 19, 2026 08:59
@rh-amarin rh-amarin changed the title savepoint Add more info to adapter framework task execution Feb 19, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Walkthrough

Adds CEL helper functions (toJson, dig) and supporting unwrap/dig logic; registers them into the CEL environment. Persists discovered nested resources as separate top-level entries keyed by nested discovery names, enriches nested resources with parent status (statusFeedback/conditions), and emits warnings on key collisions. Adds label-selector filtering to dry-run discovery, logs API request payloads, and introduces tests and many dry-run test fixtures validating the new CEL helpers, nested discovery enrichment, and discovery failure handling.

Sequence Diagram(s)

sequenceDiagram
    participant Executor as ResourceExecutor
    participant APICall as ApplyAPI
    participant Discovery as DiscoveryTransport
    participant Manifest as ManifestEnrichment
    participant Storage as ExecutionContext.Resources

    Executor->>APICall: Apply resource
    APICall-->>Executor: Apply result
    Executor->>Discovery: Discover resources after apply
    alt Discovery Failure
        Discovery-->>Executor: error
        Executor->>Executor: mark result failed\nattach error to Result\nset ExecutionError
        Executor-->>Executor: return executor error
    else Discovery Success
        Discovery-->>Executor: parent + nestedResults
        Executor->>Storage: store parent resource (by resource name)
        loop for each nested in nestedResults
            Executor->>Manifest: EnrichWithResourceStatus(parent, nested)
            Manifest-->>Executor: enriched nested resource
            Executor->>Storage: store enriched nested (by nested discovery name)
            Executor->>Executor: warn on key collisions if any
        end
        Executor-->>Executor: return success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

lgtm, approved

Suggested reviewers

  • crizzo71
  • Mischulee
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add more info to adapter framework task execution' is directly related to the PR objectives, which describe adding custom CEL functions, nested discovery storage, improved error handling, and debug logging to the adapter framework.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/executor/utils.go (1)

135-175: ⚠️ Potential issue | 🟠 Major

Debug logging of raw request payloads can leak sensitive data.

Request bodies often contain credentials or PII (tokens, passwords, secrets are present in the codebase); logging them verbatim at debug level risks exposing secrets in logs. No redaction helpers exist in the codebase. Consider logging only payload size instead of content.

🛡️ Safer logging (size‑only)
-		log.Debugf(ctx, "API call payload: %s %s payload=%s", apiCall.Method, url, string(body))
+		log.Debugf(ctx, "API call payload: %s %s payload_bytes=%d", apiCall.Method, url, len(body))
@@
-		log.Debugf(ctx, "API call payload: %s %s payload=%s", apiCall.Method, url, string(body))
+		log.Debugf(ctx, "API call payload: %s %s payload_bytes=%d", apiCall.Method, url, len(body))
@@
-		log.Debugf(ctx, "API call payload: %s %s payload=%s", apiCall.Method, url, string(body))
+		log.Debugf(ctx, "API call payload: %s %s payload_bytes=%d", apiCall.Method, url, len(body))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/executor/utils.go` around lines 135 - 175, The debug logs in the
HTTP handlers (the log.Debugf calls in the POST/PUT/PATCH cases) and the POST
error logging are printing raw request bodies which may contain secrets; change
these to avoid logging payload content by logging only the payload size
(len(body)) or a redacted placeholder instead. Specifically, update the
log.Debugf invocations around apiClient.Post/Put/Patch and the error handling
that uses logger.WithErrorField so they do not include string(body) — use the
byte length or a constant like "<redacted>" and keep renderTemplateBytes/use of
apiCall.Body/rendered body unchanged except for logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/testdata/dryrun/dryrun-cel-showcase-task-config.yaml`:
- Around line 308-310: The CEL expression for observed_generation references a
non-existent variable generationSpec; update the expression under
observed_generation (the expression field) to reference the actual parameter
name generationValue used in this config so CEL evaluates correctly (ensure the
expression string is changed from "generationSpec" to "generationValue").

In `@test/testdata/dryrun/dryrun-kubernetes-task-config.yaml`:
- Around line 184-186: The CEL expression for observed_generation uses an
undefined identifier "generationSpec"; update the expression to reference the
defined parameter name "generationValue" instead (i.e., change the expression on
observed_generation to use generationValue). Locate the observed_generation
block in the YAML (the key observed_generation and its expression) and replace
"generationSpec" with "generationValue" so CEL evaluation uses the correct
parameter.

In `@test/testdata/dryrun/dryrun-maestro-adapter-config.yaml`:
- Around line 26-28: The comment above the httpServerAddress entry is incorrect:
it says “HTTPS server address” while the value is http://localhost:8100; update
the sample so they match by either changing the comment to “HTTP server address
for REST API operations (optional)” or changing the value to use
https://localhost:8100, and ensure the httpServerAddress key and the Environment
variable reference (HYPERFLEET_MAESTRO_HTTP_SERVER_ADDRESS) remain consistent.

---

Outside diff comments:
In `@internal/executor/utils.go`:
- Around line 135-175: The debug logs in the HTTP handlers (the log.Debugf calls
in the POST/PUT/PATCH cases) and the POST error logging are printing raw request
bodies which may contain secrets; change these to avoid logging payload content
by logging only the payload size (len(body)) or a redacted placeholder instead.
Specifically, update the log.Debugf invocations around apiClient.Post/Put/Patch
and the error handling that uses logger.WithErrorField so they do not include
string(body) — use the byte length or a constant like "<redacted>" and keep
renderTemplateBytes/use of apiCall.Body/rendered body unchanged except for
logging.

Comment on lines +184 to +186
observed_generation:
expression: "generationSpec"
observed_time: "{{ now | date \"2006-01-02T15:04:05Z07:00\" }}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, search for generationSpec and generationValue across the repository
rg -n "generationSpec" -S
rg -n "generationValue" -S

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 2554


🏁 Script executed:

# Read the specific section of the file around lines 184-186
sed -n '20,30p' test/testdata/dryrun/dryrun-kubernetes-task-config.yaml
sed -n '180,190p' test/testdata/dryrun/dryrun-kubernetes-task-config.yaml

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 729


observed_generation references undefined generationSpec.
The parameter is named generationValue (defined at line 23); the CEL expression at line 185 should reference generationValue instead. Using the undefined generationSpec will cause CEL evaluation to fail or produce empty output.

🛠️ Proposed fix
          observed_generation:
-           expression: "generationSpec"
+           expression: "generationValue"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
observed_generation:
expression: "generationSpec"
observed_time: "{{ now | date \"2006-01-02T15:04:05Z07:00\" }}"
observed_generation:
expression: "generationValue"
observed_time: "{{ now | date \"2006-01-02T15:04:05Z07:00\" }}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/testdata/dryrun/dryrun-kubernetes-task-config.yaml` around lines 184 -
186, The CEL expression for observed_generation uses an undefined identifier
"generationSpec"; update the expression to reference the defined parameter name
"generationValue" instead (i.e., change the expression on observed_generation to
use generationValue). Locate the observed_generation block in the YAML (the key
observed_generation and its expression) and replace "generationSpec" with
"generationValue" so CEL evaluation uses the correct parameter.

Comment on lines +26 to +28
# HTTPS server address for REST API operations (optional)
# Environment variable: HYPERFLEET_MAESTRO_HTTP_SERVER_ADDRESS
httpServerAddress: "http://localhost:8100"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align the comment with the actual scheme (HTTP vs HTTPS).

Line 26 says “HTTPS server address” but Line 28 uses http://…. This is a minor doc mismatch that can mislead readers of the sample config.

✏️ Suggested fix
-      # HTTPS server address for REST API operations (optional)
+      # HTTP/HTTPS server address for REST API operations (optional)
       # Environment variable: HYPERFLEET_MAESTRO_HTTP_SERVER_ADDRESS
       httpServerAddress: "http://localhost:8100"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# HTTPS server address for REST API operations (optional)
# Environment variable: HYPERFLEET_MAESTRO_HTTP_SERVER_ADDRESS
httpServerAddress: "http://localhost:8100"
# HTTP/HTTPS server address for REST API operations (optional)
# Environment variable: HYPERFLEET_MAESTRO_HTTP_SERVER_ADDRESS
httpServerAddress: "http://localhost:8100"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/testdata/dryrun/dryrun-maestro-adapter-config.yaml` around lines 26 -
28, The comment above the httpServerAddress entry is incorrect: it says “HTTPS
server address” while the value is http://localhost:8100; update the sample so
they match by either changing the comment to “HTTP server address for REST API
operations (optional)” or changing the value to use https://localhost:8100, and
ensure the httpServerAddress key and the Environment variable reference
(HYPERFLEET_MAESTRO_HTTP_SERVER_ADDRESS) remain consistent.

@@ -1,4 +1,9 @@
# Invalid task configuration for testing validate command error reporting
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the renamed filename: adatepr should be adapter.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/executor/resource_executor_test.go (1)

417-421: Consider using safer type assertions in test verification.

The chained type assertions (sf["values"].([]interface{}), values[0].(map[string]interface{})) will panic if the structure differs from expectations. While this may be acceptable in tests (a panic still fails the test), using require assertions would provide clearer failure messages.

Safer assertion approach
-	sf := nested.Object["statusFeedback"].(map[string]interface{})
-	values := sf["values"].([]interface{})
-	assert.Len(t, values, 1)
-	v0 := values[0].(map[string]interface{})
-	assert.Equal(t, "data", v0["name"])
+	sf, ok := nested.Object["statusFeedback"].(map[string]interface{})
+	require.True(t, ok, "statusFeedback should be a map")
+	values, ok := sf["values"].([]interface{})
+	require.True(t, ok, "statusFeedback.values should be a slice")
+	require.Len(t, values, 1)
+	v0, ok := values[0].(map[string]interface{})
+	require.True(t, ok, "first value should be a map")
+	assert.Equal(t, "data", v0["name"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/executor/resource_executor_test.go` around lines 417 - 421, Change
the direct chained type assertions to guarded checks using the comma-ok pattern
and require helpers so failures produce clear test errors: verify that
nested.Object["statusFeedback"] is present and is a map before assigning to sf,
use the comma-ok style to extract sf["values"] and require.True/require.NotNil
(and require.Len) instead of direct .([]interface{}), then check the first
element exists and is a map with another comma-ok and require assertions before
accessing v0["name"]; reference the variables sf, values and v0 and replace bare
casts with these guarded checks (and require.Len(t, values, 1)) so type
mismatches yield readable test failures rather than panics.
internal/executor/utils.go (1)

143-143: Debug logging of request payloads is useful, but consider sensitive data exposure.

The payload may contain credentials, tokens, or PII rendered from templates. While debug-level logging is typically disabled in production, ensure log configurations enforce this to prevent accidental exposure in non-production environments with elevated log levels.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/executor/utils.go` at line 143, The current debug log call
log.Debugf(ctx, "API call payload: %s %s payload=%s", apiCall.Method, url,
string(body)) may leak sensitive data; change it to redact or sanitize the
request payload before logging (e.g., implement and call a helper like
redactSensitivePayload(body) or maskKnownFields such as tokens/credentials/PII)
and ensure the log is emitted only when debug is truly enabled (check the
logger's debug level or use an IsDebugEnabled(ctx) guard). Update the logging
call to use the sanitized payload and keep references to apiCall.Method and url
intact so the context remains useful for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/testdata/dryrun/dryrun-maestro-adapter-task-config.yaml`:
- Around line 143-148: The manifest name/namespace and the resourceIdentifier
disagree when clusterId can contain uppercase letters: resourceIdentifier uses
"{{ .clusterId | lower }}" but the ConfigMap manifest uses "{{ .clusterId }}";
update them to be consistent—either remove the | lower from resourceIdentifier
or (recommended) make the manifest use the same lowercasing transformation by
changing the ConfigMap's name and namespace to "{{ .clusterId | lower }}" so the
manifest entries and resourceIdentifier (name/namespace) match exactly.
- Around line 209-211: The nested discovery for configmap0 uses byName: "{{
.clusterId }}-config-symbol2222" which is inconsistent with the
resourceIdentifier that lowercases clusterId; update the byName template in the
discovery block for "configmap0" to also apply | lower to .clusterId (i.e., use
the same lowercase transformation as resourceIdentifier) so discovery remains
consistent when clusterId contains uppercase characters.

---

Duplicate comments:
In `@test/testdata/dryrun/dryrun-cel-showcase-task-config.yaml`:
- Around line 307-310: The CEL expression for observed_generation incorrectly
references a non-existent symbol generationSpec; update the expression under
observed_generation (the expression field) to reference the defined parameter
generationValue instead (e.g., replace generationSpec with generationValue) so
CEL evaluation uses the correct parameter name; ensure any other references in
the same YAML to generationSpec are similarly updated to generationValue to
maintain consistency.

In `@test/testdata/dryrun/dryrun-kubernetes-task-config.yaml`:
- Around line 184-186: The CEL expression for observed_generation references a
non-existent symbol generationSpec; update the expression to reference the
defined parameter generationValue (i.e., change the expression on
observed_generation from "generationSpec" to "generationValue") so CEL
evaluation uses the correct identifier (look for observed_generation and the
parameter definition generationValue).

---

Nitpick comments:
In `@internal/executor/resource_executor_test.go`:
- Around line 417-421: Change the direct chained type assertions to guarded
checks using the comma-ok pattern and require helpers so failures produce clear
test errors: verify that nested.Object["statusFeedback"] is present and is a map
before assigning to sf, use the comma-ok style to extract sf["values"] and
require.True/require.NotNil (and require.Len) instead of direct
.([]interface{}), then check the first element exists and is a map with another
comma-ok and require assertions before accessing v0["name"]; reference the
variables sf, values and v0 and replace bare casts with these guarded checks
(and require.Len(t, values, 1)) so type mismatches yield readable test failures
rather than panics.

In `@internal/executor/utils.go`:
- Line 143: The current debug log call log.Debugf(ctx, "API call payload: %s %s
payload=%s", apiCall.Method, url, string(body)) may leak sensitive data; change
it to redact or sanitize the request payload before logging (e.g., implement and
call a helper like redactSensitivePayload(body) or maskKnownFields such as
tokens/credentials/PII) and ensure the log is emitted only when debug is truly
enabled (check the logger's debug level or use an IsDebugEnabled(ctx) guard).
Update the logging call to use the sanitized payload and keep references to
apiCall.Method and url intact so the context remains useful for debugging.

Comment on lines +143 to +148
name: "{{ .clusterId }}-config-symbol2222"
namespace: "{{ .clusterId }}"
labels:
hyperfleet.io/cluster-id: "{{ .clusterId }}"
annotations:
hyperfleet.io/generation: "{{ .generationValue }}"
hyperfleet.io/generation: "{{ .generation }}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ConfigMap manifest name/namespace may not match resourceIdentifier.

The ConfigMap manifest (lines 143-144) uses:

  • name: "{{ .clusterId }}-config-symbol2222"
  • namespace: "{{ .clusterId }}"

But the resourceIdentifier (lines 187-188) uses:

  • namespace: "{{ .clusterId | lower }}"
  • name: "{{ .clusterId | lower }}-config-symbol2222"

If clusterId contains uppercase characters, the manifest and resourceIdentifier will produce different values, causing the manifestConfig to not match the actual resource.

🛠️ Proposed fix
            - apiVersion: v1
              kind: ConfigMap
              metadata:
-               name: "{{ .clusterId }}-config-symbol2222"
-               namespace: "{{ .clusterId }}"
+               name: "{{ .clusterId | lower }}-config-symbol2222"
+               namespace: "{{ .clusterId | lower }}"
                labels:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: "{{ .clusterId }}-config-symbol2222"
namespace: "{{ .clusterId }}"
labels:
hyperfleet.io/cluster-id: "{{ .clusterId }}"
annotations:
hyperfleet.io/generation: "{{ .generationValue }}"
hyperfleet.io/generation: "{{ .generation }}"
name: "{{ .clusterId | lower }}-config-symbol2222"
namespace: "{{ .clusterId | lower }}"
labels:
hyperfleet.io/cluster-id: "{{ .clusterId }}"
annotations:
hyperfleet.io/generation: "{{ .generation }}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/testdata/dryrun/dryrun-maestro-adapter-task-config.yaml` around lines
143 - 148, The manifest name/namespace and the resourceIdentifier disagree when
clusterId can contain uppercase letters: resourceIdentifier uses "{{ .clusterId
| lower }}" but the ConfigMap manifest uses "{{ .clusterId }}"; update them to
be consistent—either remove the | lower from resourceIdentifier or (recommended)
make the manifest use the same lowercasing transformation by changing the
ConfigMap's name and namespace to "{{ .clusterId | lower }}" so the manifest
entries and resourceIdentifier (name/namespace) match exactly.

Comment on lines +209 to +211
- name: "configmap0"
discovery:
byName: "{{ .clusterId }}-symbol222"
byName: "{{ .clusterId }}-config-symbol2222"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Discovery byName should also use | lower for consistency.

The nested discovery at line 211 uses byName: "{{ .clusterId }}-config-symbol2222" without | lower, but the resourceIdentifier uses | lower. This inconsistency could cause discovery to fail if clusterId contains uppercase characters.

🛠️ Proposed fix
        - name: "configmap0"
          discovery:
-           byName: "{{ .clusterId }}-config-symbol2222"
+           byName: "{{ .clusterId | lower }}-config-symbol2222"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/testdata/dryrun/dryrun-maestro-adapter-task-config.yaml` around lines
209 - 211, The nested discovery for configmap0 uses byName: "{{ .clusterId
}}-config-symbol2222" which is inconsistent with the resourceIdentifier that
lowercases clusterId; update the byName template in the discovery block for
"configmap0" to also apply | lower to .clusterId (i.e., use the same lowercase
transformation as resourceIdentifier) so discovery remains consistent when
clusterId contains uppercase characters.

@openshift-ci
Copy link

openshift-ci bot commented Feb 19, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: ciaranRoche

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-merge-bot openshift-merge-bot bot merged commit 1ce63b5 into openshift-hyperfleet:main Feb 19, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants