diff --git a/sagemaker-core/src/sagemaker/core/jumpstart/configs.py b/sagemaker-core/src/sagemaker/core/jumpstart/configs.py index d5852f0bed..9e0a53ddc6 100644 --- a/sagemaker-core/src/sagemaker/core/jumpstart/configs.py +++ b/sagemaker-core/src/sagemaker/core/jumpstart/configs.py @@ -35,6 +35,9 @@ class JumpStartConfig(BaseConfig): model_version (Optional[str]): The version of the JumpStart model. Defaults to None. hub_name (Optional[str]): The name of the JumpStart hub. Defaults to None. + hub_content_name (Optional[str]): The name of the hub content reference in the + private hub, when it differs from the public model_id. Defaults to None, + which means the hub content is expected to be named after the model_id. accept_eula (Optional[bool]): Whether to accept the EULA. Defaults to None. training_config_name (Optional[str]): The name of the training configuration. Defaults to None. @@ -45,6 +48,7 @@ class JumpStartConfig(BaseConfig): model_id: str model_version: Optional[str] = None hub_name: Optional[str] = None + hub_content_name: Optional[str] = None accept_eula: Optional[bool] = False training_config_name: Optional[str] = None inference_config_name: Optional[str] = None diff --git a/sagemaker-serve/src/sagemaker/serve/model_builder.py b/sagemaker-serve/src/sagemaker/serve/model_builder.py index 275b09707e..59c1fd2aa9 100644 --- a/sagemaker-serve/src/sagemaker/serve/model_builder.py +++ b/sagemaker-serve/src/sagemaker/serve/model_builder.py @@ -3910,6 +3910,7 @@ def from_jumpstart_config( mb_instance.resource_requirements = resource_requirements mb_instance.model_kms_key = model_kms_key mb_instance.hub_name = jumpstart_config.hub_name + mb_instance.hub_content_name = getattr(jumpstart_config, "hub_content_name", None) if mb_instance.hub_name and not getattr(mb_instance, "hub_arn", None): from sagemaker.core.jumpstart.hub.utils import ( generate_hub_arn_for_init_kwargs, diff --git a/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py b/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py index efa74261b8..08a77e0d1e 100644 --- a/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py +++ b/sagemaker-serve/src/sagemaker/serve/model_builder_servers.py @@ -988,6 +988,11 @@ def _build_for_jumpstart(self) -> Model: hub_arn = getattr(self, "hub_arn", None) if hub_arn: init_kwargs_params["hub_arn"] = hub_arn + # When the private hub content reference is named differently from + # the public model_id, resolve hub content by its actual name. + hub_content_name = getattr(self, "hub_content_name", None) + if hub_content_name: + init_kwargs_params["model_id"] = hub_content_name init_kwargs = get_init_kwargs(**init_kwargs_params) # Configure image URI and environment variables diff --git a/sagemaker-serve/tests/integ/test_private_hub_artifact_resolution.py b/sagemaker-serve/tests/integ/test_private_hub_artifact_resolution.py index 4292b6b560..13ff401bcd 100644 --- a/sagemaker-serve/tests/integ/test_private_hub_artifact_resolution.py +++ b/sagemaker-serve/tests/integ/test_private_hub_artifact_resolution.py @@ -204,3 +204,228 @@ def test_build_resolves_artifacts_via_private_hub(private_hub, execution_role, s f"{model_data_str}. Expected private hub artifact resolution." ) logger.info("Model data resolved to: %s", model_data_str) + + +ALIASED_CONTENT_NAME = "sdk-integ-aliased-phi4-mini" +NO_S3_ROLE_PREFIX = "sdk-integ-no-s3-role" + +_NO_S3_ROLE_POLICY = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ], + "Resource": "*", + }, + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Resource": "*", + }, + { + "Effect": "Allow", + "Action": [ + "sagemaker:DescribeHub", + "sagemaker:DescribeHubContent", + "sagemaker:ListHubContents", + "sagemaker:ListHubContentVersions", + ], + "Resource": "*", + }, + ], +} + + +@pytest.fixture(scope="module") +def no_s3_execution_role(): + """Create an execution role with NO S3 permissions whatsoever. + + This encodes the customer-visible contract of private hub brokered + access: with HubAccessConfig in the CreateModel call, SageMaker + brokers model data access through the hub content reference, so the + execution role needs no s3:GetObject on the public JumpStart cache. + """ + import json + + iam = boto3.client("iam", region_name=TEST_REGION) + role_name = f"{NO_S3_ROLE_PREFIX}-{uuid.uuid4().hex[:8]}" + trust = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "sagemaker.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + try: + resp = iam.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=json.dumps(trust), + Description="SDK integ test: private hub deploy with zero S3 access", + ) + iam.put_role_policy( + RoleName=role_name, + PolicyName="minimal-no-s3", + PolicyDocument=json.dumps(_NO_S3_ROLE_POLICY), + ) + except ClientError as e: + pytest.skip(f"Cannot create IAM role (likely missing permissions): {e}") + + time.sleep(15) # IAM propagation + yield resp["Role"]["Arn"] + + try: + iam.delete_role_policy(RoleName=role_name, PolicyName="minimal-no-s3") + iam.delete_role(RoleName=role_name) + except Exception as e: + logger.warning("Role cleanup failed: %s", e) + + +@pytest.fixture(scope="module") +def aliased_model_reference(private_hub): + """Add a ModelReference whose HubContentName differs from the model_id.""" + sm = boto3.client("sagemaker", region_name=TEST_REGION) + public_arn = ( + f"arn:aws:sagemaker:{TEST_REGION}:aws:hub-content/" + f"SageMakerPublicHub/Model/{TEST_MODEL_ID}" + ) + try: + sm.create_hub_content_reference( + HubName=private_hub, + SageMakerPublicHubContentArn=public_arn, + HubContentName=ALIASED_CONTENT_NAME, + ) + except ClientError as e: + pytest.skip(f"Cannot create aliased hub content reference: {e}") + + for _ in range(60): + try: + contents = sm.list_hub_contents( + HubName=private_hub, HubContentType="ModelReference" + ) + if any( + s["HubContentName"] == ALIASED_CONTENT_NAME + and s.get("HubContentStatus") == "Available" + for s in contents.get("HubContentSummaries", []) + ): + break + except Exception: + pass + time.sleep(3) + else: + pytest.skip(f"Aliased reference {ALIASED_CONTENT_NAME} not available") + + yield ALIASED_CONTENT_NAME + # Teardown handled by the private_hub fixture (deletes all references). + + +def _deploy_and_assert_hub_access_config( + hub_name, role_arn, sagemaker_session, hub_content_name=None +): + """Build + deploy with the given role; assert HubAccessConfig on the model. + + Returns after cleaning up the endpoint, endpoint config, and model. + """ + suffix = uuid.uuid4().hex[:8] + endpoint_name = f"sdk-integ-private-hub-{suffix}" + from sagemaker.train.configs import Compute + + config_kwargs = dict( + model_id=TEST_MODEL_ID, + model_version=TEST_MODEL_VERSION, + hub_name=hub_name, + accept_eula=True, + ) + if hub_content_name: + config_kwargs["hub_content_name"] = hub_content_name + + mb = ModelBuilder.from_jumpstart_config( + jumpstart_config=JumpStartConfig(**config_kwargs), + compute=Compute(instance_type=TEST_INSTANCE_TYPE), + sagemaker_session=sagemaker_session, + role_arn=role_arn, + ) + + sm = boto3.client("sagemaker", region_name=TEST_REGION) + try: + mb.build(sagemaker_session=sagemaker_session) + # deploy(wait=False): CreateModel is the call under test. It fails + # synchronously without HubAccessConfig when the role has no S3 access. + mb.deploy(endpoint_name=endpoint_name, wait=False) + + # Assert the created Model resource carries HubAccessConfig + endpoint = sm.describe_endpoint(EndpointName=endpoint_name) + ep_config = sm.describe_endpoint_config( + EndpointConfigName=endpoint["EndpointConfigName"] + ) + model_name = ep_config["ProductionVariants"][0]["ModelName"] + model = sm.describe_model(ModelName=model_name) + container = model.get("PrimaryContainer") or model["Containers"][0] + hub_access = ( + container.get("ModelDataSource", {}) + .get("S3DataSource", {}) + .get("HubAccessConfig") + ) + assert hub_access is not None, ( + "CreateModel succeeded but the model has no " + "S3DataSource.HubAccessConfig; private hub access was not brokered" + ) + assert hub_name in hub_access["HubContentArn"] + finally: + for op, kwargs in ( + (sm.delete_endpoint, {"EndpointName": endpoint_name}), + (sm.delete_endpoint_config, {"EndpointConfigName": endpoint_name}), + ): + try: + op(**kwargs) + except Exception as e: + logger.warning("Cleanup failed for %s: %s", kwargs, e) + + +@pytest.mark.slow_test +def test_deploy_with_no_s3_execution_role( + private_hub, no_s3_execution_role, sagemaker_session +): + """E2E: deploy from a private hub with an execution role that has ZERO + S3 permissions. Passes only when the SDK attaches HubAccessConfig to + the CreateModel call (SageMaker brokers artifact access via the hub). + + Verified against the v3.16.0 baseline: fails at the HubAccessConfig + assertion (the created Model resource has no + S3DataSource.HubAccessConfig, so artifact access is not brokered and + the endpoint cannot serve without public-bucket S3 permissions). + """ + _deploy_and_assert_hub_access_config( + hub_name=private_hub, + role_arn=no_s3_execution_role, + sagemaker_session=sagemaker_session, + ) + + +@pytest.mark.slow_test +def test_deploy_with_aliased_hub_content_name( + private_hub, aliased_model_reference, no_s3_execution_role, sagemaker_session +): + """E2E: deploy a ModelReference whose HubContentName differs from the + public model_id, using JumpStartConfig.hub_content_name. + + On v3.16.0 this fails at hub content resolution with ResourceNotFound + because the SDK only looks up hub content by model_id. + """ + _deploy_and_assert_hub_access_config( + hub_name=private_hub, + role_arn=no_s3_execution_role, + sagemaker_session=sagemaker_session, + hub_content_name=aliased_model_reference, + ) diff --git a/sagemaker-serve/tests/unit/test_private_hub_artifact_resolution.py b/sagemaker-serve/tests/unit/test_private_hub_artifact_resolution.py index ab6073a975..02d5856dda 100644 --- a/sagemaker-serve/tests/unit/test_private_hub_artifact_resolution.py +++ b/sagemaker-serve/tests/unit/test_private_hub_artifact_resolution.py @@ -367,95 +367,279 @@ def test_from_jumpstart_config_then_build_uses_private_hub( self.assertNotIn("jumpstart-cache-prod", mb.s3_model_data_url) -class TestModelReferenceArnPropagation(unittest.TestCase): - """Test that _build_for_jumpstart propagates model_reference_arn to self. +MOCK_MODEL_REFERENCE_ARN = ( + "arn:aws:sagemaker:us-east-1:123456789012:hub-content/" + "my-private-hub/ModelReference/huggingface-llm-phi-4-mini-instruct/1.1.0" +) +MOCK_HUB_CONTENT_NAME = "my-team-phi4-mini" +MOCK_IMAGE_URI = ( + "763104351884.dkr.ecr.us-east-1.amazonaws.com/djl-inference:0.27.0-lmi10.0.0-cu124" +) + - This is the fix for V2271554863: without propagation, _prepare_container_def_base - cannot attach HubAccessConfig to the CreateModel request, causing SageMaker to - fall back to direct S3 access on jumpstart-cache-prod-* → AccessDenied. +def _init_kwargs_mock(model_reference_arn): + """Build a get_init_kwargs return value with an explicit model_reference_arn. + + Uses an explicit value (string or None) rather than relying on Mock + auto-attributes, which are always truthy. """ + mock_init_kwargs = Mock() + mock_init_kwargs.image_uri = MOCK_IMAGE_URI + mock_init_kwargs.env = {} + mock_init_kwargs.model_data = { + "S3DataSource": { + "S3Uri": "s3://jumpstart-cache-prod-us-east-1/artifacts/model/", + "S3DataType": "S3Prefix", + "CompressionType": "None", + } + } + mock_init_kwargs.enable_network_isolation = None + mock_init_kwargs.model_reference_arn = model_reference_arn + return mock_init_kwargs - MOCK_MODEL_REFERENCE_ARN = ( - "arn:aws:sagemaker:us-east-1:123456789012:hub-content/" - "my-private-hub/ModelReference/huggingface-llm-phi-4-mini-instruct/1.1.0" - ) - def setUp(self): - self.mock_session = _mock_session() +def _build_jumpstart_builder( + sagemaker_session, model_reference_arn, hub_arn=MOCK_HUB_ARN, hub_content_name=None +): + """Run _build_for_jumpstart on a ModelBuilder with get_init_kwargs mocked. - @_PATCH_IS_JS - @patch("sagemaker.core.jumpstart.utils.validate_model_id_and_get_type", return_value=None) - @patch("sagemaker.core.jumpstart.factory.utils.get_init_kwargs") - @patch("sagemaker.serve.model_builder.ModelBuilder._create_model") - @patch("sagemaker.serve.model_builder.ModelBuilder._prepare_for_mode") - def test_model_reference_arn_propagated_to_builder( - self, mock_prepare, mock_create, mock_get_kwargs, mock_validate, mock_is_js + Returns (builder, mock_get_kwargs). Only the factory resolution and + model creation are mocked; the builder attribute plumbing under test + runs for real. + """ + with _PATCH_IS_JS, patch( + "sagemaker.core.jumpstart.utils.validate_model_id_and_get_type", + return_value=None, + ), patch( + "sagemaker.core.jumpstart.factory.utils.get_init_kwargs" + ) as mock_get_kwargs, patch( + "sagemaker.serve.model_builder.ModelBuilder._create_model" + ) as mock_create, patch( + "sagemaker.serve.model_builder.ModelBuilder._prepare_for_mode" ): - """model_reference_arn from get_init_kwargs must be set on self.""" - mock_init_kwargs = Mock() - mock_init_kwargs.image_uri = ( - "763104351884.dkr.ecr.us-east-1.amazonaws.com/djl-inference:0.27.0-lmi10.0.0-cu124" - ) - mock_init_kwargs.env = {} - mock_init_kwargs.model_data = { - "S3DataSource": { - "S3Uri": "s3://my-private-hub-bucket/artifacts/model.tar.gz", - "S3DataType": "S3Prefix", - "CompressionType": "None", - } - } - mock_init_kwargs.enable_network_isolation = None - mock_init_kwargs.model_reference_arn = self.MOCK_MODEL_REFERENCE_ARN - mock_get_kwargs.return_value = mock_init_kwargs + mock_get_kwargs.return_value = _init_kwargs_mock(model_reference_arn) mock_create.return_value = Mock() builder = ModelBuilder( model=MOCK_MODEL_ID, role_arn=MOCK_ROLE_ARN, - sagemaker_session=self.mock_session, + sagemaker_session=sagemaker_session, mode=Mode.SAGEMAKER_ENDPOINT, ) builder._optimizing = False - builder.hub_name = MOCK_HUB_NAME - builder.hub_arn = MOCK_HUB_ARN builder.model_version = MOCK_MODEL_VERSION + if hub_arn: + builder.hub_name = MOCK_HUB_NAME + builder.hub_arn = hub_arn + if hub_content_name: + builder.hub_content_name = hub_content_name builder._build_for_jumpstart() + return builder, mock_get_kwargs + + +class TestModelReferenceArnPropagation(unittest.TestCase): + """Regression tests: model_reference_arn resolved by get_init_kwargs must be + propagated onto the builder so the container definition attaches + S3DataSource.HubAccessConfig.HubContentArn in the CreateModel call. - self.assertEqual(builder.model_reference_arn, self.MOCK_MODEL_REFERENCE_ARN) + Without this propagation, the execution role is forced to have + s3:GetObject on the public jumpstart-cache-prod bucket, defeating + private hub brokered access. (Missed by PR #5985.) + """ + + def setUp(self): + self.mock_session = _mock_session() + + def test_model_reference_arn_propagated_from_init_kwargs(self): + """The ARN resolved by the factory must land on the builder instance. + + This is the test that would have caught the v3.15.1-v3.16.0 regression: + hub_arn was forwarded to get_init_kwargs (PR #5985), but the resulting + model_reference_arn was dropped, so CreateModel never got HubAccessConfig. + """ + builder, _ = _build_jumpstart_builder( + self.mock_session, model_reference_arn=MOCK_MODEL_REFERENCE_ARN + ) + + self.assertEqual( + getattr(builder, "model_reference_arn", None), + MOCK_MODEL_REFERENCE_ARN, + "model_reference_arn from get_init_kwargs was not propagated to the " + "builder; the container definition will be missing " + "S3DataSource.HubAccessConfig.HubContentArn", + ) + + def test_model_reference_arn_absent_for_public_catalog(self): + """No private hub: model_reference_arn must remain unset (public path unchanged).""" + builder, _ = _build_jumpstart_builder( + self.mock_session, model_reference_arn=None, hub_arn=None + ) + + self.assertIsNone(getattr(builder, "model_reference_arn", None)) + + +class TestContainerDefAttachesHubAccessConfig(unittest.TestCase): + """The downstream attachment point: container_def() must inject + HubAccessConfig.HubContentArn into the S3DataSource when a + model_reference_arn is provided. Pure function, no AWS calls. + """ + + def test_hub_access_config_attached(self): + from sagemaker.core.helper.session_helper import container_def + + c_def = container_def( + MOCK_IMAGE_URI, + model_data_url={ + "S3DataSource": { + "S3Uri": "s3://jumpstart-cache-prod-us-east-1/artifacts/model/", + "S3DataType": "S3Prefix", + "CompressionType": "None", + } + }, + model_reference_arn=MOCK_MODEL_REFERENCE_ARN, + ) + + self.assertEqual( + c_def["ModelDataSource"]["S3DataSource"]["HubAccessConfig"], + {"HubContentArn": MOCK_MODEL_REFERENCE_ARN}, + ) + + def test_no_hub_access_config_without_model_reference_arn(self): + from sagemaker.core.helper.session_helper import container_def + + c_def = container_def( + MOCK_IMAGE_URI, + model_data_url={ + "S3DataSource": { + "S3Uri": "s3://jumpstart-cache-prod-us-east-1/artifacts/model/", + "S3DataType": "S3Prefix", + "CompressionType": "None", + } + }, + ) + + self.assertNotIn("HubAccessConfig", c_def["ModelDataSource"]["S3DataSource"]) + + +class TestHubContentNameSupport(unittest.TestCase): + """Aliased hub content references: when the private hub content reference + is named differently from the public model_id, the SDK must resolve hub + content by its actual name (hub_content_name), not the model_id. + """ + + def setUp(self): + self.mock_session = _mock_session() + + def test_hub_content_name_used_as_lookup_model_id(self): + """When hub_content_name is set, get_init_kwargs must receive it as model_id.""" + _, mock_get_kwargs = _build_jumpstart_builder( + self.mock_session, + model_reference_arn=MOCK_MODEL_REFERENCE_ARN, + hub_content_name=MOCK_HUB_CONTENT_NAME, + ) + + call_kwargs = mock_get_kwargs.call_args.kwargs + self.assertEqual(call_kwargs.get("model_id"), MOCK_HUB_CONTENT_NAME) + + def test_model_id_used_when_no_hub_content_name(self): + """Without hub_content_name, the model_id is used for the hub lookup.""" + _, mock_get_kwargs = _build_jumpstart_builder( + self.mock_session, model_reference_arn=MOCK_MODEL_REFERENCE_ARN + ) + + call_kwargs = mock_get_kwargs.call_args.kwargs + self.assertEqual(call_kwargs.get("model_id"), MOCK_MODEL_ID) @_PATCH_IS_JS + @patch("sagemaker.serve.model_builder._retrieve_model_deploy_kwargs", return_value={}) @patch("sagemaker.core.jumpstart.utils.validate_model_id_and_get_type", return_value=None) - @patch("sagemaker.core.jumpstart.factory.utils.get_init_kwargs") - @patch("sagemaker.serve.model_builder.ModelBuilder._create_model") - @patch("sagemaker.serve.model_builder.ModelBuilder._prepare_for_mode") - def test_model_reference_arn_stays_none_without_hub( - self, mock_prepare, mock_create, mock_get_kwargs, mock_validate, mock_is_js + @patch( + "sagemaker.core.jumpstart.hub.utils.generate_hub_arn_for_init_kwargs", + return_value=MOCK_HUB_ARN, + ) + def test_from_jumpstart_config_threads_hub_content_name( + self, mock_generate_arn, mock_validate, mock_deploy_kwargs, mock_is_js ): - """Without a private hub, model_reference_arn should remain None.""" - mock_init_kwargs = Mock() - mock_init_kwargs.image_uri = ( - "763104351884.dkr.ecr.us-east-1.amazonaws.com/djl-inference:0.27.0-lmi10.0.0-cu124" + """JumpStartConfig.hub_content_name must be threaded onto the builder.""" + js_config = JumpStartConfig( + model_id=MOCK_MODEL_ID, + model_version=MOCK_MODEL_VERSION, + hub_name=MOCK_HUB_NAME, + hub_content_name=MOCK_HUB_CONTENT_NAME, ) - mock_init_kwargs.env = {} - mock_init_kwargs.model_data = "s3://jumpstart-cache-prod-us-east-1/model.tar.gz" - mock_init_kwargs.enable_network_isolation = None - mock_init_kwargs.model_reference_arn = None - mock_get_kwargs.return_value = mock_init_kwargs - mock_create.return_value = Mock() - builder = ModelBuilder( - model=MOCK_MODEL_ID, + mb = ModelBuilder.from_jumpstart_config( + jumpstart_config=js_config, role_arn=MOCK_ROLE_ARN, - sagemaker_session=self.mock_session, - mode=Mode.SAGEMAKER_ENDPOINT, + sagemaker_session=_mock_session(), ) - builder._optimizing = False - builder.model_version = MOCK_MODEL_VERSION - builder._build_for_jumpstart() + self.assertEqual( + getattr(mb, "hub_content_name", None), MOCK_HUB_CONTENT_NAME + ) - self.assertIsNone(getattr(builder, "model_reference_arn", None)) + +class TestCreateModelContainerDefinition(unittest.TestCase): + """Full-chain test: after a private hub build, the container definition + that the SDK sends to the CreateModel API must carry + S3DataSource.HubAccessConfig.HubContentArn. + + This exercises the real path end to end at the unit level: + _build_for_jumpstart -> self.model_reference_arn -> + _prepare_container_def_base -> container_def -> CreateModel payload. + No individual link is mocked between the builder attribute and the + final dict. This is the payload-shape assertion that, had it existed, + would have caught the regression that shipped in v3.15.1-v3.16.0. + """ + + def setUp(self): + self.mock_session = _mock_session() + # _prepare_container_def_base consults session config lookups that + # iterate the config object; a bare Mock is not iterable. + self.mock_session.config = None + + def _container_def_after_build(self, model_reference_arn, hub_arn=MOCK_HUB_ARN): + builder, _ = _build_jumpstart_builder( + self.mock_session, model_reference_arn=model_reference_arn, hub_arn=hub_arn + ) + # A JumpStart hub deploy has no custom inference code to upload; + # ensure the code-upload branch (which would call S3) is not taken. + builder.source_dir = None + builder.dependencies = None + builder.entry_point = None + builder.git_config = None + # _prepare_container_def_base builds the exact container definition + # dict passed to the CreateModel API as PrimaryContainer/Containers. + return builder._prepare_container_def_base() + + def test_create_model_container_def_includes_hub_access_config(self): + """Private hub build: CreateModel payload must include HubAccessConfig.""" + c_def = self._container_def_after_build( + model_reference_arn=MOCK_MODEL_REFERENCE_ARN + ) + + self.assertIn("ModelDataSource", c_def) + s3_data_source = c_def["ModelDataSource"]["S3DataSource"] + self.assertEqual( + s3_data_source.get("HubAccessConfig"), + {"HubContentArn": MOCK_MODEL_REFERENCE_ARN}, + "The container definition sent to CreateModel is missing " + "HubAccessConfig; the execution role would need s3:GetObject on " + "the public JumpStart cache bucket, defeating private hub " + "brokered access.", + ) + + def test_create_model_container_def_no_hub_access_config_for_public(self): + """Public catalog build: CreateModel payload must NOT include HubAccessConfig.""" + c_def = self._container_def_after_build( + model_reference_arn=None, hub_arn=None + ) + + self.assertIn("ModelDataSource", c_def) + self.assertNotIn( + "HubAccessConfig", c_def["ModelDataSource"]["S3DataSource"] + ) if __name__ == "__main__":