-
Notifications
You must be signed in to change notification settings - Fork 302
Add test suite for tekton-kueue CEL expressions #7561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add hack/test-tekton-kueue-config.py with parameterized unittest - Test all CEL mutation scenarios: multi-platform, priority assignment, queue config - Dynamically read image from kustomization and config from development env - Add GitHub action to run tests on kueue-related PRs Validates tekton-kueue mutations against expected annotations, labels, and priority classes using actual container via podman. Also, update the tekton-kueue config on dev to match staging. Assisted-By: Cursor Signed-off-by: Gal Ben Haim <[email protected]>
Code Review by Gemini## Review of Changes
The changes introduce a comprehensive test suite for Tekton-Kueue CEL expressions, a new GitHub Actions workflow to run these tests, and a minor update to the `tekton-kueue` configuration. The overall approach is excellent, providing robust validation for the mutation logic.
### `components/kueue/development/tekton-kueue/config.yaml`
**Improvement:**
The change to include `replace(..., "_", "-")` is a good improvement for normalizing platform names, ensuring consistency in annotation keys.
### `hack/test-tekton-kueue-config.py`
**Potential Issue/Improvement:**
The `get_tekton_kueue_image` function could be made more robust to handle different `kustomization.yaml` structures for image overrides. Currently, it strictly requires both `newName` and `newTag` to be present. If `newName` is omitted (meaning the original `name` should be used), the current logic would fail.
**Suggested Change:**
```diff
--- a/hack/test-tekton-kueue-config.py
+++ b/hack/test-tekton-kueue-config.py
@@ -29,16 +29,19 @@
# Look for the tekton-kueue image in the images section
images = kustomization.get('images', [])
for image in images:
if image.get('name') == 'konflux-ci/tekton-kueue':
- new_name = image.get('newName', '')
- new_tag = image.get('newTag', '')
- if new_name and new_tag:
- return f"{new_name}:{new_tag}"
+ # Use 'name' as fallback for 'newName' if 'newName' is not specified
+ image_name = image.get('newName', image.get('name'))
+ image_tag = image.get('newTag')
+
+ if image_name and image_tag:
+ return f"{image_name}:{image_tag}"
+ else:
+ raise ValueError(f"Incomplete tekton-kueue image definition found in kustomization. Expected 'newTag' and optionally 'newName' for '{image.get('name')}'.")
- raise ValueError("tekton-kueue image not found in kustomization")
+ raise ValueError("tekton-kueue image entry not found in kustomization for 'konflux-ci/tekton-kueue'")
except Exception as e:
raise RuntimeError(f"Failed to read tekton-kueue image from {KUSTOMIZATION_FILE}: {e}")
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: gbenhaim, mshaposhnik The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
/test appstudio-e2e-tests |
CONFIG_FILE = REPO_ROOT / "components/kueue/development/tekton-kueue/config.yaml" | ||
KUSTOMIZATION_FILE = REPO_ROOT / "components/kueue/staging/base/tekton-kueue/kustomization.yaml" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can rely on pathlib to inject the correct path separator for the current OS
CONFIG_FILE = REPO_ROOT / "components/kueue/development/tekton-kueue/config.yaml" | |
KUSTOMIZATION_FILE = REPO_ROOT / "components/kueue/staging/base/tekton-kueue/kustomization.yaml" | |
CONFIG_FILE = REPO_ROOT / "components" / "kueue" / "development" / "tekton-kueue" / "config.yaml" | |
KUSTOMIZATION_FILE = REPO_ROOT / "components" / "kueue" / "staging" / "base" / "tekton-kueue" / "kustomization.yaml"``` |
print("Checking prerequisites...") | ||
|
||
# Check config file | ||
if CONFIG_FILE.exists(): | ||
print(f"✓ Config file found: {CONFIG_FILE}") | ||
else: | ||
print(f"✗ Config file not found: {CONFIG_FILE}") | ||
sys.exit(1) | ||
|
||
# Check kustomization file | ||
if KUSTOMIZATION_FILE.exists(): | ||
print(f"✓ Kustomization file found: {KUSTOMIZATION_FILE}") | ||
else: | ||
print(f"✗ Kustomization file not found: {KUSTOMIZATION_FILE}") | ||
sys.exit(1) | ||
|
||
# Check tekton-kueue image | ||
try: | ||
image = get_tekton_kueue_image() | ||
print(f"✓ Tekton-kueue image: {image}") | ||
except Exception as e: | ||
print(f"✗ Failed to get tekton-kueue image: {e}") | ||
sys.exit(1) | ||
|
||
# Check podman | ||
try: | ||
result = subprocess.run(["podman", "--version"], capture_output=True, check=True, text=True) | ||
print(f"✓ Podman available: {result.stdout.strip()}") | ||
except (subprocess.CalledProcessError, FileNotFoundError): | ||
print("✗ Podman is not available") | ||
sys.exit(1) | ||
|
||
print("\n✅ All prerequisites met! Ready to run tests.") | ||
print("Run: python hack/test-tekton-kueue-config.py") | ||
print("\nNote: Tests will FAIL (not skip) if any prerequisites are missing.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: for readability, I'd suggest moving this into a dedicated function
def checkPrerequisites():
# ...
print("Checking prerequisites...") | |
# Check config file | |
if CONFIG_FILE.exists(): | |
print(f"✓ Config file found: {CONFIG_FILE}") | |
else: | |
print(f"✗ Config file not found: {CONFIG_FILE}") | |
sys.exit(1) | |
# Check kustomization file | |
if KUSTOMIZATION_FILE.exists(): | |
print(f"✓ Kustomization file found: {KUSTOMIZATION_FILE}") | |
else: | |
print(f"✗ Kustomization file not found: {KUSTOMIZATION_FILE}") | |
sys.exit(1) | |
# Check tekton-kueue image | |
try: | |
image = get_tekton_kueue_image() | |
print(f"✓ Tekton-kueue image: {image}") | |
except Exception as e: | |
print(f"✗ Failed to get tekton-kueue image: {e}") | |
sys.exit(1) | |
# Check podman | |
try: | |
result = subprocess.run(["podman", "--version"], capture_output=True, check=True, text=True) | |
print(f"✓ Podman available: {result.stdout.strip()}") | |
except (subprocess.CalledProcessError, FileNotFoundError): | |
print("✗ Podman is not available") | |
sys.exit(1) | |
print("\n✅ All prerequisites met! Ready to run tests.") | |
print("Run: python hack/test-tekton-kueue-config.py") | |
print("\nNote: Tests will FAIL (not skip) if any prerequisites are missing.") | |
checkPrerequisites() |
"""Set up test class - check prerequisites.""" | ||
# Check if config file exists | ||
if not CONFIG_FILE.exists(): | ||
raise FileNotFoundError(f"Config file not found: {CONFIG_FILE}") | ||
|
||
# Check if kustomization file exists | ||
if not KUSTOMIZATION_FILE.exists(): | ||
raise FileNotFoundError(f"Kustomization file not found: {KUSTOMIZATION_FILE}") | ||
|
||
# Get the tekton-kueue image from kustomization | ||
try: | ||
cls.tekton_kueue_image = get_tekton_kueue_image() | ||
print(f"Using tekton-kueue image: {cls.tekton_kueue_image}") | ||
except Exception as e: | ||
raise RuntimeError(f"Failed to get tekton-kueue image: {e}") | ||
|
||
# Check if podman is available | ||
try: | ||
subprocess.run(["podman", "--version"], capture_output=True, check=True) | ||
except FileNotFoundError: | ||
raise RuntimeError("podman command not found. Please install podman.") | ||
except subprocess.CalledProcessError as e: | ||
raise RuntimeError(f"podman is not working properly: {e}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems to me this logic is a duplicate of the check prerequisite block in main. We could move it to a dedicated function and call it from here too.
Also, as this logic is executed as part of unit tests too, does it make sense to suggest in documentation at L12 to call python hack/test-tekton-kueue-config.py --check-setup
before python hack/test-tekton-kueue-config.py
?
# Check annotations | ||
annotations = mutated.get("metadata", {}).get("annotations", {}) | ||
for expected_annotation in expected["annotations"]: | ||
self.assertIn(expected_annotation, annotations, | ||
f"Expected annotation {expected_annotation} not found") | ||
self.assertEqual(annotations[expected_annotation], "1", | ||
f"Expected annotation {expected_annotation} to have value '1'") | ||
|
||
# Check labels | ||
labels = mutated.get("metadata", {}).get("labels", {}) | ||
for expected_label, expected_value in expected["labels"].items(): | ||
self.assertIn(expected_label, labels, | ||
f"Expected label {expected_label} not found") | ||
self.assertEqual(labels[expected_label], expected_value, | ||
f"Expected label {expected_label} to have value '{expected_value}', got '{labels.get(expected_label)}'") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest checking the whole result of the mutation with assertDictEqual
# Check annotations | |
annotations = mutated.get("metadata", {}).get("annotations", {}) | |
for expected_annotation in expected["annotations"]: | |
self.assertIn(expected_annotation, annotations, | |
f"Expected annotation {expected_annotation} not found") | |
self.assertEqual(annotations[expected_annotation], "1", | |
f"Expected annotation {expected_annotation} to have value '1'") | |
# Check labels | |
labels = mutated.get("metadata", {}).get("labels", {}) | |
for expected_label, expected_value in expected["labels"].items(): | |
self.assertIn(expected_label, labels, | |
f"Expected label {expected_label} not found") | |
self.assertEqual(labels[expected_label], expected_value, | |
f"Expected label {expected_label} to have value '{expected_value}', got '{labels.get(expected_label)}'") | |
self.assertDictEqual(mutated, expected) |
This will require some changes in the test data too.
I tested with the following changes: git diff. Expand this block, that's collapsed for better readability.
diff --git a/hack/test-tekton-kueue-config.py b/hack/test-tekton-kueue-config.py
index 02d7be8f..0e736fbe 100755
--- a/hack/test-tekton-kueue-config.py
+++ b/hack/test-tekton-kueue-config.py
@@ -116,15 +116,37 @@ TEST_PIPELINERUNS = {
}
},
"expected": {
- "annotations": [
- "kueue.konflux-ci.dev/requests-linux-amd64",
- "kueue.konflux-ci.dev/requests-linux-arm64",
- "kueue.konflux-ci.dev/requests-linux-s390x"
- ],
- "labels": {
- "kueue.x-k8s.io/queue-name": "pipelines-queue",
- "kueue.x-k8s.io/priority-class": "konflux-post-merge-build"
- }
+ "apiVersion": "tekton.dev/v1",
+ "kind": "PipelineRun",
+ "metadata": {
+ "name": "test-multiplatform-new",
+ "namespace": "default",
+ "creationTimestamp": None,
+ "annotations": {
+ "kueue.konflux-ci.dev/requests-linux-amd64": "1",
+ "kueue.konflux-ci.dev/requests-linux-arm64": "1",
+ "kueue.konflux-ci.dev/requests-linux-s390x": "1",
+ },
+ "labels": {
+ "pipelinesascode.tekton.dev/event-type": "push",
+ "kueue.x-k8s.io/queue-name": "pipelines-queue",
+ "kueue.x-k8s.io/priority-class": "konflux-post-merge-build",
+ }
+ },
+ "spec": {
+ "status": "PipelineRunPending",
+ "taskRunTemplate": {},
+ "pipelineRef": {"name": "build-pipeline"},
+ "params": [
+ {
+ "name": "build-platforms",
+ "value": ["linux/amd64", "linux/arm64", "linux/s390x"]
+ },
+ {"name": "other-param", "value": "test"}
+ ],
+ "workspaces": [{"name": "shared-workspace", "emptyDir": {}}]
+ },
+ "status": {},
}
},
@@ -149,7 +171,7 @@ TEST_PIPELINERUNS = {
"taskRef": {"name": "build-task"}
},
{
- "name": "build-task-arm64",
+ "name": "build-task-arm64",
"params": [{"name": "PLATFORM", "value": "linux/arm64"}],
"taskRef": {"name": "build-task"}
},
@@ -163,14 +185,46 @@ TEST_PIPELINERUNS = {
}
},
"expected": {
- "annotations": [
- "kueue.konflux-ci.dev/requests-linux-amd64",
- "kueue.konflux-ci.dev/requests-linux-arm64"
- ],
- "labels": {
- "kueue.x-k8s.io/queue-name": "pipelines-queue",
- "kueue.x-k8s.io/priority-class": "konflux-pre-merge-build"
- }
+ "apiVersion": "tekton.dev/v1",
+ "kind": "PipelineRun",
+ "metadata": {
+ "name": "test-multiplatform-old",
+ "namespace": "default",
+ "creationTimestamp": None,
+ "annotations": {
+ "kueue.konflux-ci.dev/requests-linux-amd64": "1",
+ "kueue.konflux-ci.dev/requests-linux-arm64": "1",
+ },
+ "labels": {
+ "pipelinesascode.tekton.dev/event-type": "pull_request",
+ "kueue.x-k8s.io/queue-name": "pipelines-queue",
+ "kueue.x-k8s.io/priority-class": "konflux-pre-merge-build"
+ },
+ },
+ "spec": {
+ "status": "PipelineRunPending",
+ "taskRunTemplate": {},
+ "pipelineSpec": {
+ "tasks": [
+ {
+ "name": "build-task-amd64",
+ "params": [{"name": "PLATFORM", "value": "linux/amd64"}],
+ "taskRef": {"name": "build-task"}
+ },
+ {
+ "name": "build-task-arm64",
+ "params": [{"name": "PLATFORM", "value": "linux/arm64"}],
+ "taskRef": {"name": "build-task"}
+ },
+ {
+ "name": "other-task",
+ "taskRef": {"name": "other-task"}
+ }
+ ]
+ },
+ "workspaces": [{"name": "shared-workspace", "emptyDir": {}}]
+ },
+ "status": {},
}
},
@@ -193,11 +247,26 @@ TEST_PIPELINERUNS = {
}
},
"expected": {
- "annotations": [],
- "labels": {
- "kueue.x-k8s.io/queue-name": "pipelines-queue",
- "kueue.x-k8s.io/priority-class": "konflux-release"
- }
+ "apiVersion": "tekton.dev/v1",
+ "kind": "PipelineRun",
+ "metadata": {
+ "name": "test-release-managed",
+ "namespace": "default",
+ "creationTimestamp": None,
+ "labels": {
+ "kueue.x-k8s.io/queue-name": "pipelines-queue",
+ "kueue.x-k8s.io/priority-class": "konflux-release",
+ "appstudio.openshift.io/service": "release",
+ "pipelines.appstudio.openshift.io/type": "managed"
+ }
+ },
+ "spec": {
+ "status": "PipelineRunPending",
+ "taskRunTemplate": {},
+ "pipelineRef": {"name": "release-pipeline"},
+ "workspaces": [{"name": "shared-workspace", "emptyDir": {}}]
+ },
+ "status": {},
}
},
@@ -220,11 +289,26 @@ TEST_PIPELINERUNS = {
}
},
"expected": {
- "annotations": [],
- "labels": {
- "kueue.x-k8s.io/queue-name": "pipelines-queue",
- "kueue.x-k8s.io/priority-class": "konflux-tenant-release"
- }
+ "apiVersion": "tekton.dev/v1",
+ "kind": "PipelineRun",
+ "metadata": {
+ "name": "test-release-tenant",
+ "namespace": "default",
+ "creationTimestamp": None,
+ "labels": {
+ "appstudio.openshift.io/service": "release",
+ "pipelines.appstudio.openshift.io/type": "tenant",
+ "kueue.x-k8s.io/queue-name": "pipelines-queue",
+ "kueue.x-k8s.io/priority-class": "konflux-tenant-release",
+ }
+ },
+ "spec": {
+ "status": "PipelineRunPending",
+ "taskRunTemplate": {},
+ "pipelineRef": {"name": "release-pipeline"},
+ "workspaces": [{"name": "shared-workspace", "emptyDir": {}}]
+ },
+ "status": {},
}
},
@@ -243,11 +327,24 @@ TEST_PIPELINERUNS = {
}
},
"expected": {
- "annotations": [],
- "labels": {
- "kueue.x-k8s.io/queue-name": "pipelines-queue",
- "kueue.x-k8s.io/priority-class": "konflux-dependency-update"
- }
+ "apiVersion": "tekton.dev/v1",
+ "kind": "PipelineRun",
+ "metadata": {
+ "name": "test-mintmaker",
+ "namespace": "mintmaker",
+ "creationTimestamp": None,
+ "labels": {
+ "kueue.x-k8s.io/queue-name": "pipelines-queue",
+ "kueue.x-k8s.io/priority-class": "konflux-dependency-update",
+ }
+ },
+ "spec": {
+ "status": "PipelineRunPending",
+ "taskRunTemplate": {},
+ "pipelineRef": {"name": "dependency-update-pipeline"},
+ "workspaces": [{"name": "shared-workspace", "emptyDir": {}}]
+ },
+ "status": {},
}
},
@@ -269,11 +366,25 @@ TEST_PIPELINERUNS = {
}
},
"expected": {
- "annotations": [],
- "labels": {
- "kueue.x-k8s.io/queue-name": "pipelines-queue",
- "kueue.x-k8s.io/priority-class": "konflux-post-merge-test"
- }
+ "apiVersion": "tekton.dev/v1",
+ "kind": "PipelineRun",
+ "metadata": {
+ "name": "test-integration-test-push",
+ "namespace": "default",
+ "creationTimestamp": None,
+ "labels": {
+ "kueue.x-k8s.io/queue-name": "pipelines-queue",
+ "kueue.x-k8s.io/priority-class": "konflux-post-merge-test",
+ "pac.test.appstudio.openshift.io/event-type": "push",
+ }
+ },
+ "spec": {
+ "status": "PipelineRunPending",
+ "taskRunTemplate": {},
+ "pipelineRef": {"name": "integration-test-pipeline"},
+ "workspaces": [{"name": "shared-workspace", "emptyDir": {}}]
+ },
+ "status": {},
}
},
@@ -295,11 +406,25 @@ TEST_PIPELINERUNS = {
}
},
"expected": {
- "annotations": [],
- "labels": {
- "kueue.x-k8s.io/queue-name": "pipelines-queue",
- "kueue.x-k8s.io/priority-class": "konflux-pre-merge-test"
- }
+ "apiVersion": "tekton.dev/v1",
+ "kind": "PipelineRun",
+ "metadata": {
+ "name": "test-integration-test-pr",
+ "namespace": "default",
+ "creationTimestamp": None,
+ "labels": {
+ "pac.test.appstudio.openshift.io/event-type": "pull_request",
+ "kueue.x-k8s.io/queue-name": "pipelines-queue",
+ "kueue.x-k8s.io/priority-class": "konflux-pre-merge-test",
+ }
+ },
+ "spec": {
+ "status": "PipelineRunPending",
+ "taskRunTemplate": {},
+ "pipelineRef": {"name": "integration-test-pipeline"},
+ "workspaces": [{"name": "shared-workspace", "emptyDir": {}}]
+ },
+ "status": {},
}
},
@@ -315,14 +440,27 @@ TEST_PIPELINERUNS = {
"spec": {
"pipelineRef": {"name": "default-pipeline"},
"workspaces": [{"name": "shared-workspace", "emptyDir": {}}]
- }
+ },
},
"expected": {
- "annotations": [],
- "labels": {
- "kueue.x-k8s.io/queue-name": "pipelines-queue",
- "kueue.x-k8s.io/priority-class": "konflux-default"
- }
+ "apiVersion": "tekton.dev/v1",
+ "kind": "PipelineRun",
+ "metadata": {
+ "name": "test-default",
+ "namespace": "default",
+ "creationTimestamp": None,
+ "labels": {
+ "kueue.x-k8s.io/queue-name": "pipelines-queue",
+ "kueue.x-k8s.io/priority-class": "konflux-default"
+ },
+ },
+ "spec": {
+ "status": "PipelineRunPending",
+ "taskRunTemplate": {},
+ "pipelineRef": {"name": "default-pipeline"},
+ "workspaces": [{"name": "shared-workspace", "emptyDir": {}}]
+ },
+ "status": {},
}
}
}
@@ -408,21 +546,7 @@ class TektonKueueMutationTest(unittest.TestCase):
mutated = self.run_mutation_test(test_data)
expected = test_data["expected"]
- # Check annotations
- annotations = mutated.get("metadata", {}).get("annotations", {})
- for expected_annotation in expected["annotations"]:
- self.assertIn(expected_annotation, annotations,
- f"Expected annotation {expected_annotation} not found")
- self.assertEqual(annotations[expected_annotation], "1",
- f"Expected annotation {expected_annotation} to have value '1'")
-
- # Check labels
- labels = mutated.get("metadata", {}).get("labels", {})
- for expected_label, expected_value in expected["labels"].items():
- self.assertIn(expected_label, labels,
- f"Expected label {expected_label} not found")
- self.assertEqual(labels[expected_label], expected_value,
- f"Expected label {expected_label} to have value '{expected_value}', got '{labels.get(expected_label)}'")
+ self.assertDictEqual(mutated, expected)
def test_all_mutations(self):
"""Test all tekton-kueue mutation scenarios."""
/hold |
/test appstudio-e2e-tests |
2 similar comments
/test appstudio-e2e-tests |
/test appstudio-e2e-tests |
Validates tekton-kueue mutations against expected annotations, labels, and priority classes using actual container via podman.
Also, update the tekton-kueue config on dev to match staging.
Assisted-By: Cursor