diff --git a/runner_manager/models/runner_group.py b/runner_manager/models/runner_group.py index eb08a99e..0168d134 100644 --- a/runner_manager/models/runner_group.py +++ b/runner_manager/models/runner_group.py @@ -1,4 +1,5 @@ import logging +import re from datetime import datetime, timedelta from typing import Any, List, Optional, Self, Union from uuid import uuid4 @@ -12,6 +13,7 @@ from githubkit.webhooks.types import WorkflowJobEvent from pydantic import BaseModel as PydanticBaseModel from pydantic import Field as PydanticField +from pydantic import validator from redis_om import Field, NotFoundError from typing_extensions import Annotated @@ -25,6 +27,8 @@ log = logging.getLogger(__name__) +regex = re.compile(r"[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?|[1-9][0-9]{0,19}") + class BaseRunnerGroup(PydanticBaseModel): name: str @@ -51,7 +55,7 @@ class BaseRunnerGroup(PydanticBaseModel): class RunnerGroup(BaseModel, BaseRunnerGroup): id: Optional[int] = Field(index=True, default=None) - name: str = Field(index=True, full_text_search=True) + name: str = Field(index=True, full_text_search=True, max_length=39) organization: str = Field(index=True, full_text_search=True) repository: Optional[str] = Field(index=True, full_text_search=True) max: int = Field(index=True, ge=1, default=20) @@ -64,6 +68,16 @@ def __post_init_post_parse__(self): if self.backend.manager is None: self.backend.manager = self.manager + @validator("name") + def validate_name(cls, v): + """Validate group name. + + A group name must match the following regex: + '[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?|[1-9][0-9]{0,19}'. + """ + assert regex.fullmatch(v), f"Group name {v} must be match of regex: {regex}" + return v + @property def runner_labels(self) -> List[RunnerLabel]: """Return self.labels as a list of RunnerLabel.""" @@ -75,16 +89,12 @@ def generate_runner_name(self) -> str: Returns a string used as the runner name. - Prefixed by the group name. - Suffixed by a random uuid. - - Limited by 63 characters. + - Match the following regex: + '[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?|[1-9][0-9]{0,19}'. - Must be unique and not already exist in the database. """ - def _generate_name() -> str: - """Generate a random name.""" - - return f"{self.name}-{uuid4()}" - - name = _generate_name() + name: str = f"{self.name}-{uuid4()}" return name def get_runners(self) -> List[Runner]: diff --git a/tests/strategies.py b/tests/strategies.py index 32cbe9a6..db43babf 100644 --- a/tests/strategies.py +++ b/tests/strategies.py @@ -1,4 +1,4 @@ -from string import ascii_letters +from string import ascii_lowercase from typing import Optional from githubkit.rest.models import Runner as GitHubRunner @@ -33,8 +33,12 @@ class Repo(Repository): license: Optional[License] = None -# Text strategy with only normal characters -Text = st.text(ascii_letters, min_size=10, max_size=10) +# Text strategy with only ascii characters +# that must start with a alphabetic character, +# and only lowercase characters +Text = st.text(ascii_lowercase, min_size=10, max_size=10).filter( + lambda x: x[0].isalpha() +) Int = st.integers(min_value=1, max_value=100) UserStrategy = st.builds( User, diff --git a/tests/unit/jobs/test_workflow_job.py b/tests/unit/jobs/test_workflow_job.py index 0daf91f2..97ae51a2 100644 --- a/tests/unit/jobs/test_workflow_job.py +++ b/tests/unit/jobs/test_workflow_job.py @@ -164,7 +164,7 @@ def test_workflow_job_queued( assert webhook.organization runner_group: RunnerGroup = RunnerGroup( organization=webhook.organization.login, - name=uuid4().hex, + name=f"queued-{uuid4().hex.lower()}", labels=webhook.workflow_job.labels, manager=settings.name, backend={"name": "base"}, diff --git a/tests/unit/models/test_runner_group.py b/tests/unit/models/test_runner_group.py index 598bf8c6..763b3d3e 100644 --- a/tests/unit/models/test_runner_group.py +++ b/tests/unit/models/test_runner_group.py @@ -2,6 +2,7 @@ from githubkit.rest.models import AuthenticationToken from githubkit.webhooks.models import WorkflowJobCompleted from hypothesis import given +from pydantic import ValidationError from redis_om import Migrator, NotFoundError from runner_manager import Runner @@ -115,3 +116,29 @@ def test_update_from_base(runner_group: RunnerGroup, github: GitHub): assert basegroup.name != runner_group.name runner_group.update(**basegroup.dict(exclude_unset=True)) assert basegroup.name == runner_group.name + + +def test_runner_group_name(): + allowed_names = [ + "test", + "test-42", + "42", + "a" * 39, + ] + forbidden_names = ["TEST", "test 42", "42-test", "a" * 40] + for name in allowed_names: + group = RunnerGroup( + name=name, + organization="test", + backend={"name": "base"}, + labels=["test"], + ) + assert group.name == name + for name in forbidden_names: + with pytest.raises(ValidationError): + RunnerGroup( + name=name, + organization="test", + backend={"name": "base"}, + labels=["test"], + )