From eeb13513d03adfc7922fb7f66e26c1067afd62ab Mon Sep 17 00:00:00 2001 From: Thomas Buchner Date: Thu, 18 Jan 2024 11:39:17 +0100 Subject: [PATCH] Support for Gardener integration tests on Azure (#38) * Support Azure in integration test pipeline Provide cleanup code for community gallery images identified from a component descriptor and provide a dedicated publishing configuration for Gardener integration tests that includes Azure. * add code for config validation --- .cicd-cli.py | 2 + cleanup.py | 49 ++++++++++++++++++- glci/az.py | 115 +++++++++++++++++++++++++++++++++++++++++++- glci/model.py | 3 +- publish.py | 28 ++++++++++- publishing-cfg.yaml | 8 +++ 6 files changed, 201 insertions(+), 4 deletions(-) diff --git a/.cicd-cli.py b/.cicd-cli.py index 36c1af0..114cb4a 100755 --- a/.cicd-cli.py +++ b/.cicd-cli.py @@ -817,6 +817,8 @@ def end_phase(name): ) else: raise ValueError(on_absent) # programming error + else: + publish.validate_publishing_configuration(manifest, cfg) phase_logger.info('publishing-cfg was found to be okay - starting publishing now') diff --git a/cleanup.py b/cleanup.py index 9368936..28a85b7 100644 --- a/cleanup.py +++ b/cleanup.py @@ -13,6 +13,7 @@ import glci.aws import glci.gcp import glci.openstack_image +import glci.az import glci.model as gm import glci.util @@ -40,7 +41,7 @@ def cleanup_image( elif release.platform == 'gcp': cleanup_function = None # cleanup_gcp_images elif release.platform == 'azure': - cleanup_function = None + cleanup_function = cleanup_azure_community_gallery_images elif release.platform == 'openstack': cleanup_function = cleanup_openstack_images_by_id elif release.platform == 'oci': @@ -215,6 +216,52 @@ def cleanup_openstack_images( ) +def cleanup_azure_community_gallery_images( + release: gm.OnlineReleaseManifest, + publishing_cfg: gm.PublishingCfg, + dry_run: bool = False +): + cfg_factory = ci.util.ctx().cfg_factory() + azure_publishing_cfg: gm.PublishingTargetAzure = publishing_cfg.target(platform=release.platform) + + azure_principal = cfg_factory.azure_service_principal( + cfg_name=azure_publishing_cfg.service_principal_cfg_name, + ) + + azure_principal_serialized = gm.AzureServicePrincipalCfg( + tenant_id=azure_principal.tenant_id(), + client_id=azure_principal.client_id(), + client_secret=azure_principal.client_secret(), + subscription_id=azure_principal.subscription_id(), + ) + + shared_gallery_cfg = cfg_factory.azure_shared_gallery( + cfg_name=azure_publishing_cfg.gallery_cfg_name, + ) + shared_gallery_cfg_serialized = gm.AzureSharedGalleryCfg( + resource_group_name=shared_gallery_cfg.resource_group_name(), + gallery_name=shared_gallery_cfg.gallery_name(), + location=shared_gallery_cfg.location(), + published_name=shared_gallery_cfg.published_name(), + description=shared_gallery_cfg.description(), + eula=shared_gallery_cfg.eula(), + release_note_uri=shared_gallery_cfg.release_note_uri(), + identifier_publisher=shared_gallery_cfg.identifier_publisher(), + identifier_offer=shared_gallery_cfg.identifier_offer(), + identifier_sku=shared_gallery_cfg.identifier_sku(), + ) + + published_gallery_images = release.published_image_metadata.published_gallery_images + + for gallery_image in published_gallery_images: + glci.az.delete_from_azure_community_gallery( + community_gallery_image_id=gallery_image.community_gallery_image_id, + service_principal_cfg=azure_principal_serialized, + shared_gallery_cfg=shared_gallery_cfg_serialized, + dry_run=dry_run + ) + + def clean_release_manifest_sets( max_age_days: int=14, cicd_cfg=None, diff --git a/glci/az.py b/glci/az.py index a178b9a..6e6153e 100644 --- a/glci/az.py +++ b/glci/az.py @@ -757,6 +757,7 @@ def publish_azure_image( storage_account_cfg: glci.model.AzureStorageAccountCfg, shared_gallery_cfg: glci.model.AzureSharedGalleryCfg, marketplace_cfg: glci.model.AzureMarketplaceCfg, + hyper_v_generations: list[glci.model.AzureHyperVGeneration], publish_to_community_gallery: bool = True, publish_to_marketplace: bool = False, ) -> glci.model.OnlineReleaseManifest: @@ -808,7 +809,7 @@ def publish_azure_image( published_gallery_images=[], ) - for hyper_v_generation in glci.model.AzureHyperVGeneration: + for hyper_v_generation in hyper_v_generations: if publish_to_marketplace: logger.info(f'Publishing Azure Marketplace image for {hyper_v_generation}...') marketplace_published_image = publish_to_azure_marketplace( @@ -836,3 +837,115 @@ def publish_azure_image( published_image.published_gallery_images.append(gallery_published_image) return dataclasses.replace(release, published_image_metadata=published_image) + + +def delete_from_azure_community_gallery( + community_gallery_image_id: str, + service_principal_cfg: glci.model.AzureServicePrincipalCfg, + shared_gallery_cfg: glci.model.AzureSharedGalleryCfg, + dry_run: bool +): + credential = ClientSecretCredential( + tenant_id=service_principal_cfg.tenant_id, + client_id=service_principal_cfg.client_id, + client_secret=service_principal_cfg.client_secret + ) + cclient = ComputeManagementClient(credential, service_principal_cfg.subscription_id) + + # unfortunately, it is not possible to obtain image information from its + # community gallery image id through the API + # so we have to dissect the string and apply some implicit knowledge about its structure + gallery_image_id_parts = community_gallery_image_id.split('/') + if len(gallery_image_id_parts) != 7: + raise RuntimeError(f"community gallery image id {community_gallery_image_id} does not follow expected semantics") + + image_community_gallery_name = gallery_image_id_parts[2] + image_definition = gallery_image_id_parts[4] + image_version = gallery_image_id_parts[6] + + # check if the gallery names from the released artefact and the publishing cfg match + configured_gallery = cclient.galleries.get( + resource_group_name=shared_gallery_cfg.resource_group_name, + gallery_name=shared_gallery_cfg.gallery_name + ) + + image_gallery_is_configured_gallery = False + for public_name in configured_gallery.sharing_profile.community_gallery_info.public_names: + if public_name == image_community_gallery_name: + image_gallery_is_configured_gallery = True + break + + if not image_gallery_is_configured_gallery: + raise RuntimeError(f"The community gallery of image {community_gallery_image_id} is not from the configured community gallery {shared_gallery_cfg.gallery_name}.") + + gallery_image_version = cclient.gallery_image_versions.get( + resource_group_name=shared_gallery_cfg.resource_group_name, + gallery_name=shared_gallery_cfg.gallery_name, + gallery_image_name=image_definition, + gallery_image_version_name=image_version + ) + + # once again, resource group and image name has to be extracted from this string + image_vhd = gallery_image_version.storage_profile.source.id + image_vhd_parts = image_vhd.split('/') + if len(image_vhd_parts) != 9: + raise RuntimeError(f"image resource string {image_vhd} does not follow expected semantics") + + image_vhd_resource_group = image_vhd_parts[4] + image_vhd_name = image_vhd_parts[8] + + if dry_run: + logger.warning(f"DRY RUN: would delete gallery image version {gallery_image_version.name}") + logger.warning(f"DRY RUN: would delete image VHD {image_vhd_name} in resource group {image_vhd_resource_group}") + else: + logger.info(f"Deleting {image_version=} for {image_definition=} in gallery {shared_gallery_cfg.gallery_name}...") + result = cclient.gallery_image_versions.begin_delete( + resource_group_name=shared_gallery_cfg.resource_group_name, + gallery_name=shared_gallery_cfg.gallery_name, + gallery_image_name=image_definition, + gallery_image_version_name=image_version + ) + logger.info('...waiting for asynchronous operation to complete') + result = result.result() + + logger.info(f"Deleting image VHD {image_vhd_name} in resource group {image_vhd_resource_group}...") + result = cclient.images.begin_delete( + resource_group_name=image_vhd_resource_group, + image_name=image_vhd_name + ) + logger.info('...waiting for asynchronous operation to complete') + result = result.result() + + # check how many image versions are present in this image definition + # if none, that delete the image definition + gallery_image_versions = cclient.gallery_image_versions.list_by_gallery_image( + resource_group_name=shared_gallery_cfg.resource_group_name, + gallery_name=shared_gallery_cfg.gallery_name, + gallery_image_name=image_definition + ) + + image_version_count = sum(1 for _ in gallery_image_versions) + if image_version_count == 0: + if dry_run: + logger.warning(f"DRY RUN: would delete {image_definition=} in gallery {shared_gallery_cfg.gallery_name}") + else: + logger.info(f"Deleting {image_definition=} in gallery {shared_gallery_cfg.gallery_name}...") + result = cclient.gallery_images.begin_delete( + resource_group_name=shared_gallery_cfg.resource_group_name, + gallery_name=shared_gallery_cfg.gallery_name, + gallery_image_name=image_definition + ) + logger.info('...waiting for asynchronous operation to complete') + result = result.result() + else: + logger.warning(f"{image_definition=} still contains {image_version_count} image versions - keeping definition") + + +def validate_azure_publishing_config( + release: glci.model.OnlineReleaseManifest, + publishing_cfg: glci.model.PublishingCfg, +): + azure_publishing_cfg: glci.model.PublishingTargetAzure = publishing_cfg.target(platform=release.platform) + + if azure_publishing_cfg.publish_to_marketplace and not azure_publishing_cfg.marketplace_cfg: + raise RuntimeError(f"Expected to publish to Azure Marketplace but no marketplace config in publishing config.") diff --git a/glci/model.py b/glci/model.py index 7a9bfbb..ae78daa 100644 --- a/glci/model.py +++ b/glci/model.py @@ -758,7 +758,8 @@ class PublishingTargetAzure: gallery_cfg_name: str storage_account_cfg_name: str service_principal_cfg_name: str - marketplace_cfg: AzureMarketplaceCfg + marketplace_cfg: typing.Optional[AzureMarketplaceCfg] + hyper_v_generations: typing.List[AzureHyperVGeneration] publish_to_marketplace: bool publish_to_community_galleries: bool platform: Platform = 'azure' # should not overwrite diff --git a/publish.py b/publish.py index f0e2253..30fb12b 100644 --- a/publish.py +++ b/publish.py @@ -67,6 +67,31 @@ def publish_image( raise +def validate_publishing_configuration( + release: gm.OnlineReleaseManifest, + cfg: gm.PublishingCfg +): + if release.platform == 'azure': + validation_function = glci.az.validate_azure_publishing_config + elif release.platform == 'ali': + validation_function = None + elif release.platform == 'aws': + validation_function = None + elif release.platform == 'gcp': + validation_function = None + elif release.platform == 'openstack': + validation_function = None + elif release.platform == 'openstackbaremetal': + validation_function = None + elif release.platform == 'oci': + validation_function = None + else: + validation_function = None + + if validation_function: + validation_function(release, cfg) + + def _publish_alicloud_image( release: gm.OnlineReleaseManifest, publishing_cfg: gm.PublishingCfg, @@ -161,8 +186,9 @@ def _publish_azure_image( storage_account_cfg=storage_account_cfg_serialized, shared_gallery_cfg=shared_gallery_cfg_serialized, marketplace_cfg=azure_publishing_cfg.marketplace_cfg, + hyper_v_generations=azure_publishing_cfg.hyper_v_generations, publish_to_community_gallery=azure_publishing_cfg.publish_to_community_galleries, - publish_to_marketplace=azure_publishing_cfg.publish_to_marketplace, + publish_to_marketplace=azure_publishing_cfg.publish_to_marketplace ) diff --git a/publishing-cfg.yaml b/publishing-cfg.yaml index f7a7aa9..96b43b2 100644 --- a/publishing-cfg.yaml +++ b/publishing-cfg.yaml @@ -37,6 +37,7 @@ gallery_cfg_name: 'gardenlinux-community-gallery' storage_account_cfg_name: 'gardenlinux-community-gallery' service_principal_cfg_name: 'gardenlinux' + hyper_v_generations: ['V1', 'V2'] marketplace_cfg: offer_id: 'gardenlinux' publisher_id: 'sap' @@ -102,3 +103,10 @@ hw_disk_bus: scsi vmware_adaptertype: paraVirtual vmware_disktype: streamOptimized + - platform: 'azure' + service_principal_cfg_name: 'gardenlinux-integration-test' + storage_account_cfg_name: 'gardenlinux-integration-test' + gallery_cfg_name: 'gardenlinux-integration-test' + hyper_v_generations: ['V1'] + publish_to_marketplace: false + publish_to_community_galleries: true