Skip to content

Commit 1ae5490

Browse files
author
Eli Davidson
committed
test(integ): E2E private hub deploys with zero-S3 role and aliased content name
Add two slow_test integration tests encoding the service-side contract the unit tests cannot cover: - test_deploy_with_no_s3_execution_role: creates an execution role with NO S3 permissions (ECR + CloudWatch Logs + hub read only), deploys from the private hub, and asserts the created Model resource carries S3DataSource.HubAccessConfig. On v3.16.0 this fails at CreateModel with 'Could not access model data at s3://jumpstart-cache-prod-...'. - test_deploy_with_aliased_hub_content_name: deploys a ModelReference whose HubContentName differs from the public model_id via the new JumpStartConfig.hub_content_name. On v3.16.0 this fails with ResourceNotFound. Both reuse the existing self-provisioning private_hub fixture (create hub + reference, tear down after) and clean up endpoints, endpoint configs, and the IAM role. Mirrors the live E2E verification performed against this branch (both scenarios deployed successfully with the zero-S3 role in us-east-1). --- X-AI-Prompt: Now where's the e2e test? X-AI-Tool: meshclaw
1 parent d179364 commit 1ae5490

1 file changed

Lines changed: 223 additions & 0 deletions

File tree

‎sagemaker-serve/tests/integ/test_private_hub_artifact_resolution.py‎

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,226 @@ def test_build_resolves_artifacts_via_private_hub(private_hub, execution_role, s
204204
f"{model_data_str}. Expected private hub artifact resolution."
205205
)
206206
logger.info("Model data resolved to: %s", model_data_str)
207+
208+
209+
ALIASED_CONTENT_NAME = "sdk-integ-aliased-phi4-mini"
210+
NO_S3_ROLE_PREFIX = "sdk-integ-no-s3-role"
211+
212+
_NO_S3_ROLE_POLICY = {
213+
"Version": "2012-10-17",
214+
"Statement": [
215+
{
216+
"Effect": "Allow",
217+
"Action": [
218+
"ecr:GetAuthorizationToken",
219+
"ecr:BatchCheckLayerAvailability",
220+
"ecr:GetDownloadUrlForLayer",
221+
"ecr:BatchGetImage",
222+
],
223+
"Resource": "*",
224+
},
225+
{
226+
"Effect": "Allow",
227+
"Action": [
228+
"logs:CreateLogGroup",
229+
"logs:CreateLogStream",
230+
"logs:PutLogEvents",
231+
],
232+
"Resource": "*",
233+
},
234+
{
235+
"Effect": "Allow",
236+
"Action": [
237+
"sagemaker:DescribeHub",
238+
"sagemaker:DescribeHubContent",
239+
"sagemaker:ListHubContents",
240+
"sagemaker:ListHubContentVersions",
241+
],
242+
"Resource": "*",
243+
},
244+
],
245+
}
246+
247+
248+
@pytest.fixture(scope="module")
249+
def no_s3_execution_role():
250+
"""Create an execution role with NO S3 permissions whatsoever.
251+
252+
This encodes the customer-visible contract of private hub brokered
253+
access: with HubAccessConfig in the CreateModel call, SageMaker
254+
brokers model data access through the hub content reference, so the
255+
execution role needs no s3:GetObject on the public JumpStart cache.
256+
"""
257+
import json
258+
259+
iam = boto3.client("iam", region_name=TEST_REGION)
260+
role_name = f"{NO_S3_ROLE_PREFIX}-{uuid.uuid4().hex[:8]}"
261+
trust = {
262+
"Version": "2012-10-17",
263+
"Statement": [
264+
{
265+
"Effect": "Allow",
266+
"Principal": {"Service": "sagemaker.amazonaws.com"},
267+
"Action": "sts:AssumeRole",
268+
}
269+
],
270+
}
271+
try:
272+
resp = iam.create_role(
273+
RoleName=role_name,
274+
AssumeRolePolicyDocument=json.dumps(trust),
275+
Description="SDK integ test: private hub deploy with zero S3 access",
276+
)
277+
iam.put_role_policy(
278+
RoleName=role_name,
279+
PolicyName="minimal-no-s3",
280+
PolicyDocument=json.dumps(_NO_S3_ROLE_POLICY),
281+
)
282+
except ClientError as e:
283+
pytest.skip(f"Cannot create IAM role (likely missing permissions): {e}")
284+
285+
time.sleep(15) # IAM propagation
286+
yield resp["Role"]["Arn"]
287+
288+
try:
289+
iam.delete_role_policy(RoleName=role_name, PolicyName="minimal-no-s3")
290+
iam.delete_role(RoleName=role_name)
291+
except Exception as e:
292+
logger.warning("Role cleanup failed: %s", e)
293+
294+
295+
@pytest.fixture(scope="module")
296+
def aliased_model_reference(private_hub):
297+
"""Add a ModelReference whose HubContentName differs from the model_id."""
298+
sm = boto3.client("sagemaker", region_name=TEST_REGION)
299+
public_arn = (
300+
f"arn:aws:sagemaker:{TEST_REGION}:aws:hub-content/"
301+
f"SageMakerPublicHub/Model/{TEST_MODEL_ID}"
302+
)
303+
try:
304+
sm.create_hub_content_reference(
305+
HubName=private_hub,
306+
SageMakerPublicHubContentArn=public_arn,
307+
HubContentName=ALIASED_CONTENT_NAME,
308+
)
309+
except ClientError as e:
310+
pytest.skip(f"Cannot create aliased hub content reference: {e}")
311+
312+
for _ in range(60):
313+
try:
314+
contents = sm.list_hub_contents(
315+
HubName=private_hub, HubContentType="ModelReference"
316+
)
317+
if any(
318+
s["HubContentName"] == ALIASED_CONTENT_NAME
319+
and s.get("HubContentStatus") == "Available"
320+
for s in contents.get("HubContentSummaries", [])
321+
):
322+
break
323+
except Exception:
324+
pass
325+
time.sleep(3)
326+
else:
327+
pytest.skip(f"Aliased reference {ALIASED_CONTENT_NAME} not available")
328+
329+
yield ALIASED_CONTENT_NAME
330+
# Teardown handled by the private_hub fixture (deletes all references).
331+
332+
333+
def _deploy_and_assert_hub_access_config(
334+
hub_name, role_arn, sagemaker_session, hub_content_name=None
335+
):
336+
"""Build + deploy with the given role; assert HubAccessConfig on the model.
337+
338+
Returns after cleaning up the endpoint, endpoint config, and model.
339+
"""
340+
suffix = uuid.uuid4().hex[:8]
341+
endpoint_name = f"sdk-integ-private-hub-{suffix}"
342+
from sagemaker.train.configs import Compute
343+
344+
config_kwargs = dict(
345+
model_id=TEST_MODEL_ID,
346+
model_version=TEST_MODEL_VERSION,
347+
hub_name=hub_name,
348+
accept_eula=True,
349+
)
350+
if hub_content_name:
351+
config_kwargs["hub_content_name"] = hub_content_name
352+
353+
mb = ModelBuilder.from_jumpstart_config(
354+
jumpstart_config=JumpStartConfig(**config_kwargs),
355+
compute=Compute(instance_type=TEST_INSTANCE_TYPE),
356+
sagemaker_session=sagemaker_session,
357+
role_arn=role_arn,
358+
)
359+
360+
sm = boto3.client("sagemaker", region_name=TEST_REGION)
361+
try:
362+
mb.build(sagemaker_session=sagemaker_session)
363+
# deploy(wait=False): CreateModel is the call under test. It fails
364+
# synchronously without HubAccessConfig when the role has no S3 access.
365+
mb.deploy(endpoint_name=endpoint_name, wait=False)
366+
367+
# Assert the created Model resource carries HubAccessConfig
368+
endpoint = sm.describe_endpoint(EndpointName=endpoint_name)
369+
ep_config = sm.describe_endpoint_config(
370+
EndpointConfigName=endpoint["EndpointConfigName"]
371+
)
372+
model_name = ep_config["ProductionVariants"][0]["ModelName"]
373+
model = sm.describe_model(ModelName=model_name)
374+
container = model.get("PrimaryContainer") or model["Containers"][0]
375+
hub_access = (
376+
container.get("ModelDataSource", {})
377+
.get("S3DataSource", {})
378+
.get("HubAccessConfig")
379+
)
380+
assert hub_access is not None, (
381+
"CreateModel succeeded but the model has no "
382+
"S3DataSource.HubAccessConfig; private hub access was not brokered"
383+
)
384+
assert hub_name in hub_access["HubContentArn"]
385+
finally:
386+
for op, kwargs in (
387+
(sm.delete_endpoint, {"EndpointName": endpoint_name}),
388+
(sm.delete_endpoint_config, {"EndpointConfigName": endpoint_name}),
389+
):
390+
try:
391+
op(**kwargs)
392+
except Exception:
393+
pass
394+
395+
396+
@pytest.mark.slow_test
397+
def test_deploy_with_no_s3_execution_role(
398+
private_hub, no_s3_execution_role, sagemaker_session
399+
):
400+
"""E2E: deploy from a private hub with an execution role that has ZERO
401+
S3 permissions. Passes only when the SDK attaches HubAccessConfig to
402+
the CreateModel call (SageMaker brokers artifact access via the hub).
403+
404+
On v3.16.0 this fails at CreateModel with a ValidationException:
405+
'Could not access model data at s3://jumpstart-cache-prod-...'.
406+
"""
407+
_deploy_and_assert_hub_access_config(
408+
hub_name=private_hub,
409+
role_arn=no_s3_execution_role,
410+
sagemaker_session=sagemaker_session,
411+
)
412+
413+
414+
@pytest.mark.slow_test
415+
def test_deploy_with_aliased_hub_content_name(
416+
private_hub, aliased_model_reference, no_s3_execution_role, sagemaker_session
417+
):
418+
"""E2E: deploy a ModelReference whose HubContentName differs from the
419+
public model_id, using JumpStartConfig.hub_content_name.
420+
421+
On v3.16.0 this fails at hub content resolution with ResourceNotFound
422+
because the SDK only looks up hub content by model_id.
423+
"""
424+
_deploy_and_assert_hub_access_config(
425+
hub_name=private_hub,
426+
role_arn=no_s3_execution_role,
427+
sagemaker_session=sagemaker_session,
428+
hub_content_name=aliased_model_reference,
429+
)

0 commit comments

Comments
 (0)