diff --git a/src/deadline_worker_agent/api_models.py b/src/deadline_worker_agent/api_models.py index 770bb244..ec4e1604 100644 --- a/src/deadline_worker_agent/api_models.py +++ b/src/deadline_worker_agent/api_models.py @@ -240,7 +240,7 @@ class PosixUser(TypedDict): """The posix group name associated with session file ownership""" -class JobsRunAs(TypedDict): +class JobRunAsUser(TypedDict): posix: PosixUser # TODO: windows support @@ -249,7 +249,11 @@ class JobDetailsData(JobDetailsIdentifierFields): jobAttachmentSettings: NotRequired[JobAttachmentQueueSettings] """The queue's job attachment settings""" - jobsRunAs: NotRequired[JobsRunAs | None] + # TODO: remove once service no longer sends this + jobsRunAs: NotRequired[JobRunAsUser | None] + """Deprecated: The queue's info on how to run the job processes (ie. posix user/group)""" + + jobRunAsUser: NotRequired[JobRunAsUser | None] """The queue's info on how to run the job processes (ie. posix user/group)""" logGroupName: str diff --git a/src/deadline_worker_agent/boto/shim.py b/src/deadline_worker_agent/boto/shim.py index 81069706..e1d7a637 100644 --- a/src/deadline_worker_agent/boto/shim.py +++ b/src/deadline_worker_agent/boto/shim.py @@ -254,7 +254,14 @@ def batch_get_job_entity( "rootPrefix": "my-queue", }, "logGroupName": "/aws/deadline/queue-abc", + # TODO: remove once service no longer sends this "jobsRunAs": { + "posix": { + "user": "", + "group": "", + } + }, + "jobRunAsUser": { "posix": { "user": "job-user", "group": "job-group", diff --git a/src/deadline_worker_agent/scheduler/scheduler.py b/src/deadline_worker_agent/scheduler/scheduler.py index 5a2b4761..357903f5 100644 --- a/src/deadline_worker_agent/scheduler/scheduler.py +++ b/src/deadline_worker_agent/scheduler/scheduler.py @@ -704,11 +704,11 @@ def _create_new_sessions( logger.debug(f"[{new_session_id}] Assigned actions") os_user: Optional[SessionUser] = None - if job_details.jobs_run_as and not self._impersonation.inactive: + if job_details.job_run_as_user and not self._impersonation.inactive: if os.name != "posix": # TODO: windows support raise NotImplementedError(f"{os.name} is not supported") - os_user = job_details.jobs_run_as.posix + os_user = job_details.job_run_as_user.posix if self._impersonation.posix_job_user is not None: os_user = self._impersonation.posix_job_user diff --git a/src/deadline_worker_agent/sessions/job_entities/job_details.py b/src/deadline_worker_agent/sessions/job_entities/job_details.py index 0195b46b..b1e3bbb0 100644 --- a/src/deadline_worker_agent/sessions/job_entities/job_details.py +++ b/src/deadline_worker_agent/sessions/job_entities/job_details.py @@ -20,7 +20,7 @@ IntParameter, JobDetailsData, JobAttachmentQueueSettings as JobAttachmentSettingsBoto, - JobsRunAs as JobsRunAsModel, + JobRunAsUser as JobRunAsUserModel, PathMappingRule, PathParameter, StringParameter, @@ -84,31 +84,31 @@ def path_mapping_api_model_to_openjd( return rules -def jobs_runs_as_api_model_to_worker_agent( - jobs_run_as_data: JobsRunAsModel | None, -) -> JobsRunAs | None: - """Converts the 'JobsRunAs' api model to the 'JobsRunAs' dataclass +def job_run_as_user_api_model_to_worker_agent( + job_run_as_user_data: JobRunAsUserModel | None, +) -> JobRunAsUser | None: + """Converts the 'JobRunAsUser' api model to the 'JobRunAsUser' dataclass expected by the Worker Agent. """ - jobs_run_as: JobsRunAs | None = None - if not jobs_run_as_data: + job_run_as_user: JobRunAsUser | None = None + if not job_run_as_user_data: return None if os.name == "posix": - jobs_run_as_posix = jobs_run_as_data.get("posix", {}) - user = jobs_run_as_posix.get("user", "") - group = jobs_run_as_posix.get("group", "") + job_run_as_user_posix = job_run_as_user_data.get("posix", {}) + user = job_run_as_user_posix.get("user", "") + group = job_run_as_user_posix.get("group", "") if not (user and group): return None - jobs_run_as = JobsRunAs( + job_run_as_user = JobRunAsUser( posix=PosixSessionUser(user=user, group=group), ) else: # TODO: windows support raise NotImplementedError(f"{os.name} is not supported") - return jobs_run_as + return job_run_as_user @dataclass(frozen=True) @@ -130,7 +130,7 @@ def from_boto(cls, data: JobAttachmentSettingsBoto) -> JobAttachmentSettings: @dataclass(frozen=True) -class JobsRunAs: +class JobRunAsUser: posix: PosixSessionUser # TODO: windows support @@ -154,7 +154,7 @@ class JobDetails: parameters: list[Parameter] = field(default_factory=list) """The job's parameters""" - jobs_run_as: JobsRunAs | None = None + job_run_as_user: JobRunAsUser | None = None """The user associated with the job's Amazon Deadline Cloud queue""" path_mapping_rules: list[OPENJDPathMappingRule] = field(default_factory=list) @@ -189,8 +189,13 @@ def from_boto(cls, job_details_data: JobDetailsData) -> JobDetails: if job_attachment_settings_boto := job_details_data.get("jobAttachmentSettings", None): job_attachment_settings = JobAttachmentSettings.from_boto(job_attachment_settings_boto) - jobs_run_as_data = job_details_data.get("jobsRunAs", None) - jobs_run_as: JobsRunAs | None = jobs_runs_as_api_model_to_worker_agent(jobs_run_as_data) + # Use jobRunAsUser if it exists, otherwise jobsRunAs if it exists, otherwise None. + job_run_as_user_data = job_details_data.get( + "jobRunAsUser", job_details_data.get("jobsRunAs", None) + ) + job_run_as_user: JobRunAsUser | None = job_run_as_user_api_model_to_worker_agent( + job_run_as_user_data + ) # Note: Record the empty string as a None as well. queue_role_arn: str | None = ( @@ -208,7 +213,7 @@ def from_boto(cls, job_details_data: JobDetailsData) -> JobDetails: parameters=job_parameters, schema_version=schema_version, log_group_name=job_details_data["logGroupName"], - jobs_run_as=jobs_run_as, + job_run_as_user=job_run_as_user, path_mapping_rules=path_mapping_rules, job_attachment_settings=job_attachment_settings, queue_role_arn=queue_role_arn, @@ -252,6 +257,7 @@ def validate_entity_data(cls, entity_data: dict[str, Any]) -> JobDetailsData: expected_type=list, required=False, ), + # TODO: remove jobsRunAs once service no longer responds with jobsRunAs Field( key="jobsRunAs", expected_type=dict, @@ -268,6 +274,22 @@ def validate_entity_data(cls, entity_data: dict[str, Any]) -> JobDetailsData: ), ), ), + Field( + key="jobRunAsUser", + expected_type=dict, + required=False, + fields=( + Field( + key="posix", + expected_type=dict, + required=False, + fields=( + Field(key="user", expected_type=str, required=True), + Field(key="group", expected_type=str, required=True), + ), + ), + ), + ), Field( key="jobAttachmentSettings", expected_type=dict, diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 6f6eb290..03667f10 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -27,7 +27,7 @@ from deadline_worker_agent.sessions.job_entities.job_details import ( JobAttachmentSettings, JobDetails, - JobsRunAs, + JobRunAsUser, ) from deadline_worker_agent.sessions.job_entities.job_attachment_details import ( JobAttachmentDetails, @@ -238,12 +238,12 @@ def job_parameters() -> list[Parameter]: @pytest.fixture -def jobs_run_as() -> JobsRunAs | None: +def job_run_as_user() -> JobRunAsUser | None: """The OS user/group associated with the job's queue""" # TODO: windows support if os.name != "posix": raise NotImplementedError(f"{os.name} is not supported") - return JobsRunAs(posix=PosixSessionUser(user="job-user", group="job-user")) + return JobRunAsUser(posix=PosixSessionUser(user="job-user", group="job-user")) @pytest.fixture @@ -257,14 +257,14 @@ def job_details( queue_job_attachment_settings: JobAttachmentSettings, job_parameters: list[Parameter], log_group_name: str, - jobs_run_as: JobsRunAs | None, + job_run_as_user: JobRunAsUser | None, path_mapping_rules: list[PathMappingRule], schema_version: SchemaVersion, ) -> JobDetails: return JobDetails( job_attachment_settings=queue_job_attachment_settings, parameters=job_parameters, - jobs_run_as=jobs_run_as, + job_run_as_user=job_run_as_user, path_mapping_rules=path_mapping_rules, log_group_name=log_group_name, schema_version=schema_version, diff --git a/test/unit/sessions/test_job_entities.py b/test/unit/sessions/test_job_entities.py index c07fefa5..5af59bcb 100644 --- a/test/unit/sessions/test_job_entities.py +++ b/test/unit/sessions/test_job_entities.py @@ -32,7 +32,7 @@ JobDetails as JobDetailsBoto, JobDetailsData, JobDetailsIdentifier, - JobsRunAs, + JobRunAsUser, PathMappingRule, StepDetails as StepDetailsBoto, StepDetailsData, @@ -174,16 +174,16 @@ def test_has_path_mapping_rules( assert job_details.path_mapping_rules not in (None, []) assert len(job_details.path_mapping_rules) == len(path_mapping_rules) - def test_jobs_run_as(self) -> None: - """Ensures that if we receive a jobs_run_as field in the response, + def test_job_run_as_user(self) -> None: + """Ensures that if we receive a job_run_as_user field in the response, that the created entity has a (Posix) SessionUser created with the proper values""" # GIVEN expected_user = "job-user" expected_group = "job-group" - entity_data: JobDetailsData = { + api_response: dict = { "jobId": "job-123", - "jobsRunAs": { + "jobRunAsUser": { "posix": { "user": expected_user, "group": expected_group, @@ -194,16 +194,124 @@ def test_jobs_run_as(self) -> None: } # WHEN - entity_obj = JobDetails.from_boto(entity_data) + job_details_data = JobDetails.validate_entity_data(api_response) + entity_obj = JobDetails.from_boto(job_details_data) # THEN - assert entity_obj.jobs_run_as is not None - assert isinstance(entity_obj.jobs_run_as.posix, PosixSessionUser) - assert entity_obj.jobs_run_as.posix.user == expected_user - assert entity_obj.jobs_run_as.posix.group == expected_group + assert entity_obj.job_run_as_user is not None + assert isinstance(entity_obj.job_run_as_user.posix, PosixSessionUser) + assert entity_obj.job_run_as_user.posix.user == expected_user + assert entity_obj.job_run_as_user.posix.group == expected_group + # TODO: remove test once service no longer sends jobsRunAs @pytest.mark.parametrize( ("jobs_run_as_data"), + ( + pytest.param( + { + "user": "", + "group": "", + }, + id="empty user and group", + ), + pytest.param( + { + "user": "diff-user", + "group": "diff-group", + }, + id="set user and group", + ), + ), + ) + def test_old_jobs_run_as_existence(self, jobs_run_as_data: dict[str, str]) -> None: + """Ensures that if we receive the old jobs_run_as field in the response, + that we do not error on validating the response and use the newer jobRunAsUser info""" + # GIVEN + expected_user = "job-user" + expected_group = "job-group" + api_response: dict = { + "jobId": "job-123", + "jobsRunAs": { + "posix": jobs_run_as_data, + }, + "jobRunAsUser": { + "posix": { + "user": expected_user, + "group": expected_group, + }, + }, + "logGroupName": "TEST", + "schemaVersion": SchemaVersion.v2023_09.value, + } + + # WHEN + job_details_data = JobDetails.validate_entity_data(api_response) + entity_obj = JobDetails.from_boto(job_details_data) + + # THEN + assert not hasattr(entity_obj, "jobs_run_as") + assert entity_obj.job_run_as_user is not None + assert isinstance(entity_obj.job_run_as_user.posix, PosixSessionUser) + assert entity_obj.job_run_as_user.posix.user == expected_user + assert entity_obj.job_run_as_user.posix.group == expected_group + + # TODO: remove once service no longer sends jobsRunAs + def test_only_old_jobs_run_as(self) -> None: + """Ensures that if we only receive the old jobs_run_as field in the response, + that we do not error on validating the response and we have job_run_as_user info""" + # GIVEN + expected_user = "job-user" + expected_group = "job-group" + api_response: dict = { + "jobId": "job-123", + "jobsRunAs": { + "posix": { + "user": expected_user, + "group": expected_group, + }, + }, + "logGroupName": "TEST", + "schemaVersion": SchemaVersion.v2023_09.value, + } + + # WHEN + job_details_data = JobDetails.validate_entity_data(api_response) + entity_obj = JobDetails.from_boto(job_details_data) + + # THEN + assert not hasattr(entity_obj, "jobs_run_as") + assert entity_obj.job_run_as_user is not None + assert isinstance(entity_obj.job_run_as_user.posix, PosixSessionUser) + assert entity_obj.job_run_as_user.posix.user == expected_user + assert entity_obj.job_run_as_user.posix.group == expected_group + + # TODO: remove once service no longer sends jobsRunAs + def test_only_empty_old_jobs_run_as(self) -> None: + """Ensures that if we only receive the old jobs_run_as field with no user and group, + that we do not error on validating the response and we do not have job_run_as_user info""" + # GIVEN + api_response: dict = { + "jobId": "job-123", + "jobsRunAs": { + "posix": { + "user": "", + "group": "", + }, + }, + "logGroupName": "TEST", + "schemaVersion": SchemaVersion.v2023_09.value, + } + + # WHEN + job_details_data = JobDetails.validate_entity_data(api_response) + entity_obj = JobDetails.from_boto(job_details_data) + + # THEN + assert not hasattr(entity_obj, "jobs_run_as") + assert entity_obj.job_run_as_user is None + + @pytest.mark.parametrize( + ("job_run_as_user_data"), ( pytest.param( { @@ -236,13 +344,13 @@ def test_jobs_run_as(self) -> None: pytest.param({}, id="no posix"), ), ) - def test_jobs_run_empty_values(self, jobs_run_as_data: JobsRunAs | None) -> None: - """Ensures that if we are missing values in the jobs_run_as fields + def test_job_run_as_user_empty_values(self, job_run_as_user_data: JobRunAsUser | None) -> None: + """Ensures that if we are missing values in the job_run_as_user fields that created entity does not have it set (ie. old queues)""" # GIVEN entity_data: JobDetailsData = { "jobId": "job-123", - "jobsRunAs": jobs_run_as_data, + "jobRunAsUser": job_run_as_user_data, "logGroupName": "TEST", "schemaVersion": SchemaVersion.v2023_09.value, } @@ -251,10 +359,10 @@ def test_jobs_run_empty_values(self, jobs_run_as_data: JobsRunAs | None) -> None entity_obj = JobDetails.from_boto(entity_data) # THEN - assert entity_obj.jobs_run_as is None + assert entity_obj.job_run_as_user is None - def test_jobs_run_as_not_provided(self) -> None: - """Ensures that if we somehow don't receive a jobs_run_as field + def test_job_run_as_user_not_provided(self) -> None: + """Ensures that if we somehow don't receive a job_run_as_user field that the created entity does not have it set (shouldn't happen)""" # GIVEN entity_data: JobDetailsData = { @@ -267,7 +375,7 @@ def test_jobs_run_as_not_provided(self) -> None: entity_obj = JobDetails.from_boto(entity_data) # THEN - assert entity_obj.jobs_run_as is None + assert entity_obj.job_run_as_user is None class TestDetails: