Skip to content

Commit 83e552a

Browse files
authored
Merge pull request #10 from release-engineering/azure_verify_submission_state
Azure: Better handle "publish_preview" and "publish_live"
2 parents 79d51c9 + 94c61c4 commit 83e552a

File tree

2 files changed

+167
-3
lines changed

2 files changed

+167
-3
lines changed

cloudpub/ms_azure/service.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from requests import HTTPError
88
from tenacity import retry
99
from tenacity.retry import retry_if_result
10-
from tenacity.stop import stop_after_delay
10+
from tenacity.stop import stop_after_attempt, stop_after_delay
1111
from tenacity.wait import wait_chain, wait_fixed
1212

1313
from cloudpub.common import BaseService
@@ -511,6 +511,11 @@ def _is_submission_in_preview(self, current: ProductSubmission) -> bool:
511511
return current.id != live.id # If they're the same then state == live
512512
return True # when no live it means it's in preview
513513

514+
@retry(
515+
wait=wait_fixed(wait=60),
516+
stop=stop_after_attempt(3),
517+
reraise=True,
518+
)
514519
def _publish_preview(self, product: Product, product_name: str) -> None:
515520
"""
516521
Submit the product to 'preview' if it's not already in this state.
@@ -536,8 +541,23 @@ def _publish_preview(self, product: Product, product_name: str) -> None:
536541
log.info(
537542
"Submitting the product \"%s (%s)\" to \"preview\"." % (product_name, product.id)
538543
)
539-
self.submit_to_status(product_id=product.id, status='preview')
544+
res = self.submit_to_status(product_id=product.id, status='preview')
545+
546+
if res.job_result != 'succeeded' or not self.get_submission_state(
547+
product.id, state="preview"
548+
):
549+
errors = "\n".join(res.errors)
550+
failure_msg = (
551+
f"Failed to submit the product {product.id} to preview. "
552+
f"Status: {res.job_result} Errors: {errors}"
553+
)
554+
raise RuntimeError(failure_msg)
540555

556+
@retry(
557+
wait=wait_fixed(wait=60),
558+
stop=stop_after_attempt(3),
559+
reraise=True,
560+
)
541561
def _publish_live(self, product: Product, product_name: str) -> None:
542562
"""
543563
Submit the product to 'live' after going through Azure Marketplace Validation.
@@ -551,7 +571,15 @@ def _publish_live(self, product: Product, product_name: str) -> None:
551571
# Note: the offer can only go `live` after successfully being changed to `preview`
552572
# which takes up to 4 days.
553573
log.info("Submitting the product \"%s (%s)\" to \"live\"." % (product_name, product.id))
554-
self.submit_to_status(product_id=product.id, status='live')
574+
res = self.submit_to_status(product_id=product.id, status='live')
575+
576+
if res.job_result != 'succeeded' or not self.get_submission_state(product.id, state="live"):
577+
errors = "\n".join(res.errors)
578+
failure_msg = (
579+
f"Failed to submit the product {product.id} to live. "
580+
f"Status: {res.job_result} Errors: {errors}"
581+
)
582+
raise RuntimeError(failure_msg)
555583

556584
def publish(self, metadata: AzurePublishingMetadata) -> None:
557585
"""

tests/ms_azure/test_service.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,134 @@ def test_ensure_can_publish_raises(
833833
with pytest.raises(RuntimeError, match=err):
834834
azure_service.ensure_can_publish("ffffffff-ffff-ffff-ffff-ffffffffffff")
835835

836+
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
837+
@mock.patch("cloudpub.ms_azure.AzureService.submit_to_status")
838+
@mock.patch("cloudpub.ms_azure.AzureService._is_submission_in_preview")
839+
def test_publish_preview_success_on_retry(
840+
self,
841+
mock_is_sbpreview: mock.MagicMock,
842+
mock_subst: mock.MagicMock,
843+
mock_getsubst: mock.MagicMock,
844+
product_obj: Product,
845+
azure_service: AzureService,
846+
) -> None:
847+
# Prepare mocks
848+
mock_config_res = mock.MagicMock()
849+
mock_config_res.job_result = "succeeded"
850+
mock_is_sbpreview.return_value = False
851+
mock_subst.side_effect = [mock_config_res for _ in range(3)]
852+
mock_getsubst.side_effect = [
853+
None, # Fail on 1st call
854+
None,
855+
mock.MagicMock(), # Success on 3rd call
856+
]
857+
# Remove the retry sleep
858+
azure_service._publish_preview.retry.sleep = mock.Mock() # type: ignore
859+
860+
# Test
861+
azure_service._publish_preview(product_obj, "test-product")
862+
863+
mock_subst.assert_has_calls(
864+
[mock.call(product_id=product_obj.id, status="preview") for _ in range(3)]
865+
)
866+
mock_getsubst.assert_has_calls(
867+
[mock.call(product_obj.id, state="preview") for _ in range(3)]
868+
)
869+
870+
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
871+
@mock.patch("cloudpub.ms_azure.AzureService.submit_to_status")
872+
@mock.patch("cloudpub.ms_azure.AzureService._is_submission_in_preview")
873+
def test_publish_preview_fail_on_retry(
874+
self,
875+
mock_is_sbpreview: mock.MagicMock,
876+
mock_subst: mock.MagicMock,
877+
mock_getsubst: mock.MagicMock,
878+
product_obj: Product,
879+
azure_service: AzureService,
880+
) -> None:
881+
# Prepare mocks
882+
err_resp = ConfigureStatus.from_json(
883+
{
884+
"jobId": "1",
885+
"jobStatus": "completed",
886+
"jobResult": "failed",
887+
"errors": ["failure1", "failure2"],
888+
}
889+
)
890+
mock_is_sbpreview.return_value = False
891+
mock_subst.side_effect = [err_resp for _ in range(3)]
892+
mock_getsubst.side_effect = [None for _ in range(3)]
893+
# Remove the retry sleep
894+
azure_service._publish_preview.retry.sleep = mock.Mock() # type: ignore
895+
expected_err = (
896+
f"Failed to submit the product {product_obj.id} to preview. "
897+
"Status: failed Errors: failure1\nfailure2"
898+
)
899+
900+
# Test
901+
with pytest.raises(RuntimeError, match=expected_err):
902+
azure_service._publish_preview(product_obj, "test-product")
903+
904+
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
905+
@mock.patch("cloudpub.ms_azure.AzureService.submit_to_status")
906+
def test_publish_live_success_on_retry(
907+
self,
908+
mock_subst: mock.MagicMock,
909+
mock_getsubst: mock.MagicMock,
910+
product_obj: Product,
911+
azure_service: AzureService,
912+
) -> None:
913+
# Prepare mocks
914+
mock_config_res = mock.MagicMock()
915+
mock_config_res.job_result = "succeeded"
916+
mock_subst.side_effect = [mock_config_res for _ in range(3)]
917+
mock_getsubst.side_effect = [
918+
None, # Fail on 1st call
919+
None,
920+
mock.MagicMock(), # Success on 3rd call
921+
]
922+
# Remove the retry sleep
923+
azure_service._publish_live.retry.sleep = mock.Mock() # type: ignore
924+
925+
# Test
926+
azure_service._publish_live(product_obj, "test-product")
927+
928+
mock_subst.assert_has_calls(
929+
[mock.call(product_id=product_obj.id, status="live") for _ in range(3)]
930+
)
931+
mock_getsubst.assert_has_calls([mock.call(product_obj.id, state="live") for _ in range(3)])
932+
933+
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
934+
@mock.patch("cloudpub.ms_azure.AzureService.submit_to_status")
935+
def test_publish_live_fail_on_retry(
936+
self,
937+
mock_subst: mock.MagicMock,
938+
mock_getsubst: mock.MagicMock,
939+
product_obj: Product,
940+
azure_service: AzureService,
941+
) -> None:
942+
# Prepare mocks
943+
err_resp = ConfigureStatus.from_json(
944+
{
945+
"jobId": "1",
946+
"jobStatus": "completed",
947+
"jobResult": "failed",
948+
"errors": ["failure1", "failure2"],
949+
}
950+
)
951+
mock_subst.side_effect = [err_resp for _ in range(3)]
952+
mock_getsubst.side_effect = [None for _ in range(3)]
953+
# Remove the retry sleep
954+
azure_service._publish_live.retry.sleep = mock.Mock() # type: ignore
955+
expected_err = (
956+
f"Failed to submit the product {product_obj.id} to live. "
957+
"Status: failed Errors: failure1\nfailure2"
958+
)
959+
960+
# Test
961+
with pytest.raises(RuntimeError, match=expected_err):
962+
azure_service._publish_live(product_obj, "test-product")
963+
836964
@mock.patch("cloudpub.ms_azure.AzureService.configure")
837965
@mock.patch("cloudpub.ms_azure.AzureService.submit_to_status")
838966
@mock.patch("cloudpub.ms_azure.service.update_skus")
@@ -1184,6 +1312,7 @@ def test_is_submission_in_preview(
11841312
mock_substt.assert_called_once_with(current.product_id, "live")
11851313

11861314
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
1315+
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
11871316
@mock.patch("cloudpub.ms_azure.AzureService.diff_offer")
11881317
@mock.patch("cloudpub.ms_azure.AzureService.configure")
11891318
@mock.patch("cloudpub.ms_azure.AzureService.submit_to_status")
@@ -1202,6 +1331,7 @@ def test_publish_live(
12021331
mock_submit: mock.MagicMock,
12031332
mock_configure: mock.MagicMock,
12041333
mock_diff_offer: mock.MagicMock,
1334+
mock_getsubst: mock.MagicMock,
12051335
mock_ensure_publish: mock.MagicMock,
12061336
product_obj: Product,
12071337
plan_summary_obj: PlanSummary,
@@ -1221,6 +1351,11 @@ def test_publish_live(
12211351
[technical_config_obj],
12221352
[submission_obj],
12231353
]
1354+
mock_getsubst.side_effect = ["preview", "live"]
1355+
mock_res_preview = mock.MagicMock()
1356+
mock_res_live = mock.MagicMock()
1357+
mock_res_preview.job_result = mock_res_live.job_result = "succeeded"
1358+
mock_submit.side_effect = [mock_res_preview, mock_res_live]
12241359
mock_is_sas.return_value = False
12251360
expected_source = VMImageSource(
12261361
source_type="sasUri",
@@ -1237,6 +1372,7 @@ def test_publish_live(
12371372
technical_config_obj.disk_versions = [disk_version_obj]
12381373
technical_config_obj.disk_versions = [disk_version_obj]
12391374

1375+
# Test
12401376
azure_service.publish(metadata_azure_obj)
12411377
mock_getprpl_name.assert_called_once_with("example-product", "plan-1")
12421378
filter_calls = [

0 commit comments

Comments
 (0)