diff --git a/azimuth_schedule_operator/models/v1alpha1/schedule.py b/azimuth_schedule_operator/models/v1alpha1/schedule.py index 66edc40..5c7f9bf 100644 --- a/azimuth_schedule_operator/models/v1alpha1/schedule.py +++ b/azimuth_schedule_operator/models/v1alpha1/schedule.py @@ -10,19 +10,19 @@ class ScheduleStatus(schema.BaseModel): ref_found: schema.Optional[bool] = False # updated when delete has been triggered delete_triggered: schema.Optional[bool] = False - updatedAt: schema.Optional[datetime.datetime] = None + updated_at: schema.Optional[datetime.datetime] = None class ScheduleRef(schema.BaseModel): - apiVersion: str + api_version: str kind: str name: str class ScheduleSpec(schema.BaseModel): ref: ScheduleRef - notBefore: datetime.datetime - notAfter: datetime.datetime + not_before: datetime.datetime + not_after: datetime.datetime class Schedule( diff --git a/azimuth_schedule_operator/operator.py b/azimuth_schedule_operator/operator.py index a887aac..e8003b0 100644 --- a/azimuth_schedule_operator/operator.py +++ b/azimuth_schedule_operator/operator.py @@ -64,29 +64,58 @@ async def delete_reference(namespace: str, ref: schedule_crd.ScheduleRef): await resource.delete(ref.name, namespace=namespace) -async def delete_after_delay(delay_seconds, namespace, ref: schedule_crd.ScheduleRef): - LOG.info(f"Delete of {ref} will be executed after {delay_seconds} seconds") +async def delete_after_delay(delay_seconds, namespace, schedule: schedule_crd.Schedule): + LOG.info( + f"Delete of {schedule.metadata.name} will be executed " + f"after {delay_seconds} seconds" + ) await asyncio.sleep(delay_seconds) - await delete_reference(namespace, ref) - # TODO(johngarbutt): update crd to say delete has triggered - LOG.info(f"Delete complete for {namespace} and {ref}.") + await delete_reference(namespace, schedule.spec.ref) + await update_schedule(schedule.metadata.name, namespace, delete_triggered=True) + LOG.info(f"Delete complete for {namespace} and {schedule.metadata.name}.") -async def schedule_delete_task(memo, namespace, schedule): +async def schedule_delete_task(memo, namespace, schedule: schedule_crd.Schedule): if memo.get("delete_task"): # TODO(johngarbutt): maybe we don't always need to cancel? memo["delete_task"].cancel() time_to_delete = datetime.timedelta(minutes=15) - scheduled_at = schedule.spec.notBefore - time_to_delete + scheduled_at = schedule.spec.not_before - time_to_delete delay_seconds = (scheduled_at - datetime.datetime.now()).total_seconds() if delay_seconds < 0: delay_seconds = 0 memo["delete_scheduled_at"] = scheduled_at memo["delete_scheduled_ref"] = schedule.spec.ref memo["delete_task"] = asyncio.create_task( - delete_after_delay(delay_seconds, namespace, schedule.spec.ref) + delete_after_delay(delay_seconds, namespace, schedule) + ) + + +async def update_schedule( + name: str, + namespace: str, + ref_found: bool = None, + delete_triggered: bool = None, +): + now = datetime.datetime.utcnow() + now_string = now.strftime("%Y-%m-%dT%H:%M:%SZ") + status_updates = dict(updated_at=now_string) + + if ref_found is not None: + status_updates["ref_found"] = ref_found + if delete_triggered is not None: + status_updates["delete_triggered"] = delete_triggered + + status_resource = await K8S_CLIENT.api(registry.API_VERSION).resource( + "schedule/status" + ) + LOG.info(f"patching {name} in {namespace} with: {status_updates}") + await status_resource.patch( + name, + dict(status=status_updates), + namespace=namespace, ) @@ -99,7 +128,7 @@ async def schedule_changed(memo: kopf.Memo, body, namespace, **_): # check we can get the object we are supposed to be managing object = await get_reference(namespace, schedule.spec.ref) LOG.info(f"object found {object}") - # TODO(johngarbutt): update object to show we have found the reference + await update_schedule(schedule.metadata.name, namespace, ref_found=True) # TODO(johngarbutt): maybe check we have an owner relationship? await schedule_delete_task(memo, namespace, schedule) diff --git a/azimuth_schedule_operator/tests/test_operator.py b/azimuth_schedule_operator/tests/test_operator.py index 0ab9cf9..7d3ead9 100644 --- a/azimuth_schedule_operator/tests/test_operator.py +++ b/azimuth_schedule_operator/tests/test_operator.py @@ -52,10 +52,11 @@ async def test_cleanup_calls_aclose(self, mock_client): await operator.cleanup() mock_client.aclose.assert_awaited_once_with() + @mock.patch.object(operator, "update_schedule") @mock.patch.object(operator, "schedule_delete_task") @mock.patch.object(operator, "get_reference") async def test_schedule_changed( - self, mock_get_reference, mock_schedule_delete_task + self, mock_get_reference, mock_schedule_delete_task, mock_update_schedule ): memo = kopf.Memo() body = schedule_crd.get_fake_dict() @@ -67,6 +68,9 @@ async def test_schedule_changed( # Assert the expected behavior mock_get_reference.assert_awaited_once_with(namespace, fake.spec.ref) mock_schedule_delete_task.assert_awaited_once_with(memo, namespace, mock.ANY) + mock_update_schedule.assert_awaited_once_with( + fake.metadata.name, namespace, ref_found=True + ) @mock.patch.object(asyncio, "create_task") @mock.patch.object(operator, "delete_after_delay", new_callable=mock.Mock) @@ -81,23 +85,29 @@ async def test_schedule_delete_task( await operator.schedule_delete_task(memo, namespace, schedule) # Assert the expected behavior - mock_delete_after_delay.assert_called_once_with(0, namespace, schedule.spec.ref) + mock_delete_after_delay.assert_called_once_with(0, namespace, schedule) self.assertEqual( memo["delete_scheduled_at"], - schedule.spec.notBefore - datetime.timedelta(minutes=15), + schedule.spec.not_before - datetime.timedelta(minutes=15), ) self.assertEqual(memo["delete_scheduled_ref"], schedule.spec.ref) mock_create_task.assert_called_once_with(mock.ANY) self.assertEqual(memo["delete_task"], "sentinel") + @mock.patch.object(operator, "update_schedule") @mock.patch.object(asyncio, "sleep") @mock.patch.object(operator, "delete_reference") - async def test_delete_after_delay(self, mock_delete_reference, mock_sleep): + async def test_delete_after_delay( + self, mock_delete_reference, mock_sleep, mock_update_schedule + ): delay_seconds = 10 namespace = "ns1" - ref = schedule_crd.get_fake().spec.ref + schedule = schedule_crd.get_fake() - await operator.delete_after_delay(delay_seconds, namespace, ref) + await operator.delete_after_delay(delay_seconds, namespace, schedule) - mock_delete_reference.assert_awaited_once_with(namespace, ref) + mock_delete_reference.assert_awaited_once_with(namespace, schedule.spec.ref) mock_sleep.assert_awaited_once_with(delay_seconds) + mock_update_schedule.assert_awaited_once_with( + schedule.metadata.name, namespace, delete_triggered=True + ) diff --git a/tools/functional_test.sh b/tools/functional_test.sh index 24a8610..4c654c0 100755 --- a/tools/functional_test.sh +++ b/tools/functional_test.sh @@ -23,5 +23,9 @@ export BEFORE=$(date --date="-2 hour" +"%Y-%m-%dT%H:%M:%SZ") export AFTER=$(date --date="-1 hour" +"%Y-%m-%dT%H:%M:%SZ") envsubst < $SCRIPT_DIR/test_schedule.yaml | kubectl apply -f - -# until kubectl wait --for=jsonpath='{.status.phase}'=Available clustertype quick-test; do echo "wait for status to appear"; sleep 5; done kubectl get schedule caas-mycluster -o yaml +# until kubectl wait --for=jsonpath='{.status.refFound}'=true schedule caas-mycluster; do echo "wait for status to appear"; sleep 5; done +# kubectl get schedule caas-mycluster -o yaml + +# for debugging get the logs from the operator +kubectl logs -n azimuth-schedule-operator deployment/azimuth-schedule-operator \ No newline at end of file