Source code for aws_lbd_art_builder_core.layer.publish
# -*- coding: utf-8 -*-"""Lambda layer publication — Step 4 of the layer workflow.Takes the uploaded ``layer.zip`` from S3 and creates a versioned Lambda layerresource via the AWS API.**Why smart publishing?**Creating a Lambda layer version is a non-reversible, append-only operation(versions can be deleted but not overwritten). Publishing identicaldependencies as a new version wastes version numbers and forces downstreamstacks to update for no reason. This module compares the local dependencymanifest against the previously stored one and skips the publish when nothinghas changed."""importtypingasTimportdataclassesfromfunctoolsimportcached_propertyfromfunc_args.apiimportBaseFrozenModelfromfunc_args.apiimportREQfrom..constantsimportS3MetadataKeyEnumfrom..importsimportS3Pathfrom..importsimportsimple_aws_lambdafrom.foundationimportLayerManifestManagerifT.TYPE_CHECKING:# pragma: no coverfrommypy_boto3_lambdaimportLambdaClient
[docs]@dataclasses.dataclass(frozen=True)classLambdaLayerVersionPublisher(LayerManifestManager):""" Command class for intelligent Lambda layer version publishing. Inherits :class:`~aws_lbd_art_builder_core.layer.foundation.LayerManifestManager` for manifest handling and adds layer publication logic. **Why Command Pattern here?** Publishing involves multiple AWS API calls with shared state (layer name, clients, manifest path, S3 layout). Keeping these as fields on a frozen dataclass makes the publisher easy to construct, inspect, and test — same rationale as the builder classes. """# fmt: offlayer_name:str=dataclasses.field(default=REQ)lambda_client:"LambdaClient"=dataclasses.field(default=REQ)publish_layer_version_kwargs:dict[str,T.Any]|None=dataclasses.field(default=None)# fmt: on
[docs]defstep_1_preflight_check(self):""" Perform read-only validation of build environment and project configuration. """self.log("--- Step 1 - Flight Check")self.step_1_1_ensure_layer_zip_exists()self.step_1_2_ensure_layer_zip_is_consistent()self.step_1_3_ensure_dependencies_have_changed()
[docs]defstep_2_publish_layer_version(self)->"LayerDeployment":""" Execute the layer publication workflow, creating a new Lambda layer version. """self.log("--- Step 2 - Publish Lambda Layer Version")layer_version,layer_version_arn=self.step_2_1_run_publish_layer_version_api()s3path_manifest=self.step_2_2_upload_dependency_manifest(version=layer_version)layer_deployment=LayerDeployment(layer_name=self.layer_name,layer_version=layer_version,layer_version_arn=layer_version_arn,s3path_manifest=s3path_manifest,)returnlayer_deployment
# --- step_1_preflight_check sub-steps
[docs]defstep_1_1_ensure_layer_zip_exists(self):""" Verify that the layer.zip file was successfully uploaded to S3. """s3path=self.s3_layout.s3path_temp_layer_zipself.log(f"--- Step 1.1 - Verify layer.zip exists in S3 at {s3path.uri}...")ifself.is_layer_zip_exists()isFalse:s3path=self.s3_layout.s3path_temp_layer_zipraiseFileNotFoundError(f"Layer zip file {s3path.uri} does not exist! "f"Please run the upload step first to create the layer.zip in S3.")else:self.log("Layer zip file found in S3.")
[docs]defis_layer_zip_exists(self)->bool:""" Check if the layer zip file exists in S3 temporary storage. :return: True if layer.zip exists in S3, False otherwise """s3path=self.s3_layout.s3path_temp_layer_zipreturns3path.exists(bsm=self.s3_client)
[docs]defstep_1_2_ensure_layer_zip_is_consistent(self):""" Validate that the uploaded layer.zip matches the current local manifest. """self.log("--- Step 1.2 - Validate layer.zip consistency with manifest")ifself.is_layer_zip_consistent()isFalse:path=self.path_manifests3path=self.s3_layout.s3path_temp_layer_zipraiseValueError(f"Layer zip file {s3path.uri} is inconsistent with current manifest {path}! "f"The uploaded layer.zip corresponds to a different dependency state. "f"Please re-run the upload step to sync the layer.zip with current dependencies.")else:self.log("Layer zip file is consistent with current manifest.")
[docs]defis_layer_zip_consistent(self)->bool:""" Compare the manifest MD5 stored in S3 metadata with the local manifest. :return: True if uploaded layer.zip matches current manifest, False otherwise """s3path=self.s3_layout.s3path_temp_layer_zips3path.head_object(bsm=self.s3_client)manifest_md5=s3path.metadata.get(S3MetadataKeyEnum.manifest_md5.value,"__invalid__")returnmanifest_md5==self.manifest_md5
[docs]defstep_1_3_ensure_dependencies_have_changed(self):""" Check if dependencies have changed since the last publication. This is the core intelligence that prevents unnecessary layer version creation — skips publishing when the manifest is identical to the previously published one. """self.log("--- Step 1.3 - Check if dependencies have changed since last publication")has_changed=self.has_dependency_manifest_changed()ifnothas_changed:raiseValueError("Dependencies unchanged since last publication - skipping")else:self.log("Dependencies have changed - proceeding with publishing.")
[docs]defhas_dependency_manifest_changed(self)->bool:""" Detect if the local dependency manifest has changed from the last published layer. :return: True if local manifest differs from latest published version, False if they are identical (no changes detected) """latest_layer_version=self.latest_layer_versioniflatest_layer_versionisNone:returnTrue# No previous version exists, treat as changedpath_manifest=self.path_manifests3path_manifest=self.get_versioned_manifest(version=latest_layer_version.version)ifs3path_manifest.exists(bsm=self.s3_client)isFalse:returnTruelocal_manifest_content=path_manifest.read_text()stored_manifest_content=s3path_manifest.read_text(bsm=self.s3_client)returnlocal_manifest_content!=stored_manifest_content
[docs]defstep_2_1_run_publish_layer_version_api(self)->tuple[int,str]:""" Publish a new Lambda layer version using the zip file stored in S3. :return: Tuple of (layer_version_number, layer_version_arn) """self.log("--- Step 2.1 - Publish new Lambda layer version via API")ifself.publish_layer_version_kwargsisNone:publish_layer_version_kwargs={}else:publish_layer_version_kwargs=self.publish_layer_version_kwargss3path=self.s3_layout.s3path_temp_layer_zipresponse=self.lambda_client.publish_layer_version(LayerName=self.layer_name,Content={"S3Bucket":s3path.bucket,"S3Key":s3path.key,},**publish_layer_version_kwargs,)layer_version_arn=response["LayerVersionArn"]layer_version=int(layer_version_arn.split(":")[-1])self.log(f"Successfully published layer version: {layer_version}")self.log(f"Layer version ARN: {layer_version_arn}")returnlayer_version,layer_version_arn
[docs]defstep_2_2_upload_dependency_manifest(self,version:int,)->"S3Path":""" Upload the dependency manifest file to S3 for the specified layer version. :param version: The layer version number to associate the manifest with :return: S3Path where the manifest was stored """self.log("--- Step 2.2 - Upload dependency manifest to S3")path=self.path_manifests3path_manifest=self.get_versioned_manifest(version=version)s3path_manifest.write_bytes(path.read_bytes(),content_type="text/plain",bsm=self.s3_client,)ifself.verbose:self.log(f"Manifest stored at: {s3path_manifest.uri}")self.log(f"Console URL: {s3path_manifest.console_url}")returns3path_manifest
[docs]@dataclasses.dataclass(frozen=True)classLayerDeployment(BaseFrozenModel):""" Immutable record of a completed layer deployment. """layer_name:str=dataclasses.field(default=REQ)layer_version:int=dataclasses.field(default=REQ)layer_version_arn:str=dataclasses.field(default=REQ)s3path_manifest:"S3Path"=dataclasses.field(default=REQ)