diff --git a/.pipelines/templates/stages/common_tasks/extension-images.yml b/.pipelines/templates/stages/common_tasks/extension-images.yml deleted file mode 100644 index fcc497ddb..000000000 --- a/.pipelines/templates/stages/common_tasks/extension-images.yml +++ /dev/null @@ -1,79 +0,0 @@ -parameters: - - name: "config" - displayName: "Trident configuration" - type: string - - - name: "deploymentEnvironment" - type: string - values: - - virtualMachine - - bareMetal - -steps: - - bash: | - set -eux - - if [ ${{ parameters.config }} != 'extensions' ]; then - echo "Skipping step. Configuration is '${{ parameters.config }}'." - exit 0 - fi - - ./bin/storm-trident helper build-extension-images --num-clones 2 - - displayName: "Build test sysext and confext images" - workingDirectory: $(Build.SourcesDirectory) - retryCountOnTaskFailure: 3 - - - task: AzureCLI@2 - inputs: - azureSubscription: trident-dev-acr-write-umi-ECF - scriptType: bash - scriptLocation: inlineScript - inlineScript: | - set -eux - - if [ ${{ parameters.config }} != 'extensions' ]; then - echo "Skipping step. Configuration is '${{ parameters.config }}'." - exit 0 - fi - - # Login to ACR - az acr login -n $(ACR_NAME) - - sysext_repository_name="sysext" - confext_repository_name="confext" - build_id="$(Build.BuildId)" - - cd $(Build.SourcesDirectory) - - tag_base="v${build_id}.${{ parameters.config }}.${{ parameters.deploymentEnvironment }}" - for version in 1 2; do - sysext_filename="test-sysext-${version}.raw" - confext_filename="test-confext-${version}.raw" - - tag="$tag_base.${version}" - - if [[ -f "$sysext_filename" ]]; then - echo "Pushing $sysext_filename with tag $tag to $(ACR_NAME).azurecr.io" - oras push $(ACR_NAME).azurecr.io/$sysext_repository_name:$tag "$sysext_filename" - sleep 3 - echo "Verifying $sysext_filename was pushed successfully..." - az acr repository show --name $(ACR_NAME) --image ${sysext_repository_name}:${tag} - else - echo "File $sysext_filename not found" - fi - if [[ -f "$confext_filename" ]]; then - echo "Pushing $confext_filename with tag $tag to $(ACR_NAME).azurecr.io" - oras push $(ACR_NAME).azurecr.io/$confext_repository_name:$tag "$confext_filename" - sleep 3 - echo "Verifying $confext_filename was pushed successfully..." - az acr repository show --name $(ACR_NAME) --image ${confext_repository_name}:${tag} - else - echo "File $confext_filename not found" - fi - done - - # Set variable for sysext and confext repositories - echo "##vso[task.setvariable variable=TAG_BASE]$tag_base" - displayName: "Push extension images to ACR" - retryCountOnTaskFailure: 3 diff --git a/.pipelines/templates/stages/common_tasks/push-to-acr.yml b/.pipelines/templates/stages/common_tasks/push-to-acr.yml index 6226eb244..2fa01761d 100644 --- a/.pipelines/templates/stages/common_tasks/push-to-acr.yml +++ b/.pipelines/templates/stages/common_tasks/push-to-acr.yml @@ -20,7 +20,25 @@ steps: - bash: | set -eux - if [ ${{ parameters.config }} != 'misc' ]; then + if [ ${{ parameters.config }} == 'extensions' ]; then + ./bin/storm-trident helper build-extension-images --build-sysexts --num-clones 2 + ls + sha384sum test-sysext-1.raw + sha384sum test-sysext-2.raw + elif [ ${{ parameters.config }} == 'root-verity' ]; then + ./bin/storm-trident helper build-extension-images --build-confexts --num-clones 2 + else + echo "Skipping step. Configuration is '${{ parameters.config }}'." + exit 0 + fi + + displayName: "Build test sysext and confext images" + workingDirectory: $(Build.SourcesDirectory) + retryCountOnTaskFailure: 3 + + - bash: | + set -eux + if [ ${{ parameters.config }} != 'misc' ] && [ ${{ parameters.config }} != 'extensions' ]; then echo "Skipping step. Configuration is '${{ parameters.config }}'." exit 0 fi @@ -42,42 +60,45 @@ steps: inlineScript: | set -eux - if [ ${{ parameters.config }} != 'misc' ]; then - echo "Skipping step. Configuration is '${{ parameters.config }}'." - exit 0 - fi - - # Login to ACR - az acr login -n $(ACR_NAME) - - repository_name=${{ parameters.imageName }} - build_id="$(Build.BuildId)" - + if [ ${{ parameters.config }} == 'misc' ]; then + ./bin/storm-trident helper acr \ + --push \ + --config ${{ parameters.config }} \ + --deployment-environment ${{ parameters.deploymentEnvironment }} \ + --acr-name $(ACR_NAME) \ + --repo-name ${{ parameters.imageName }} \ + --build-id $(Build.BuildId) \ + --file-paths $(Build.SourcesDirectory)/artifacts/test-image/regular.cosi \ + --file-paths $(Build.SourcesDirectory)/artifacts/test-image/regular_v2.cosi \ + --file-paths $(Build.SourcesDirectory)/artifacts/test-image/regular_v3.cosi \ + --file-paths $(Build.SourcesDirectory)/artifacts/test-image/regular_v4.cosi - cd $(Build.SourcesDirectory)/artifacts/test-image + elif [ ${{ parameters.config }} == 'extensions' ]; then + ./bin/storm-trident helper acr \ + --push \ + --config ${{ parameters.config }} \ + --deployment-environment ${{ parameters.deploymentEnvironment }} \ + --acr-name $(ACR_NAME) \ + --repo-name sysext \ + --build-id $(Build.BuildId) \ + --file-paths $(Build.SourcesDirectory)/test-sysext-1.raw \ + --file-paths $(Build.SourcesDirectory)/test-sysext-2.raw - tag_base="v${build_id}.${{ parameters.config }}.${{ parameters.deploymentEnvironment }}" - for version in {1..4}; do - if [[ $version == 1 ]]; then - filename="regular.cosi" - else - filename="regular_v${version}.cosi" - fi + elif [ ${{ parameters.config }} == 'root-verity' ]; then + ./bin/storm-trident helper acr \ + --push \ + --config ${{ parameters.config }} \ + --deployment-environment ${{ parameters.deploymentEnvironment }} \ + --acr-name $(ACR_NAME) \ + --repo-name confext \ + --build-id $(Build.BuildId) \ + --file-paths $(Build.SourcesDirectory)/test-confext-1.raw \ + --file-paths $(Build.SourcesDirectory)/test-confext-2.raw - tag="$tag_base.${version}" - - if [[ -f "$filename" ]]; then - echo "Pushing $filename with tag $tag to $(ACR_NAME).azurecr.io" - oras push $(ACR_NAME).azurecr.io/$repository_name:$tag "$filename" - sleep 3 - echo "Verifying $filename was pushed successfully..." - az acr repository show --name $(ACR_NAME) --image ${repository_name}:${tag} - else - echo "File $filename not found" - fi - done + else + echo "Skipping step. Configuration is '${{ parameters.config }}'." + exit 0 + fi - # Set variable for tag base of pushed images - echo "##vso[task.setvariable variable=TAG_BASE]$tag_base" displayName: "Push to ACR" retryCountOnTaskFailure: 3 diff --git a/.pipelines/templates/stages/common_tasks/remove-from-acr.yml b/.pipelines/templates/stages/common_tasks/remove-from-acr.yml index 808c6f571..d99f671ca 100644 --- a/.pipelines/templates/stages/common_tasks/remove-from-acr.yml +++ b/.pipelines/templates/stages/common_tasks/remove-from-acr.yml @@ -10,6 +10,12 @@ parameters: displayName: "Trident configuration" type: string + - name: "deploymentEnvironment" + type: string + values: + - virtualMachine + - bareMetal + steps: - task: AzureCLI@2 inputs: @@ -19,39 +25,40 @@ steps: inlineScript: | set -eux - if [ ${{ parameters.config }} != 'misc' ] && [ "${{ parameters.config }}" != 'extensions' ]; then + if [ ${{ parameters.config }} == 'misc' ]; then + # Remove COSI images + ./bin/storm-trident helper acr \ + --remove \ + --config ${{ parameters.config }} \ + --deployment-environment ${{ parameters.deploymentEnvironment }} \ + --acr-name $(ACR_NAME) \ + --repo-name ${{ parameters.repository }} \ + --build-id $(Build.BuildId) \ + --num-clones 4 + + elif [ ${{ parameters.config }} == 'extensions' ]; then + ./bin/storm-trident helper acr \ + --remove \ + --config ${{ parameters.config }} \ + --deployment-environment ${{ parameters.deploymentEnvironment }} \ + --acr-name $(ACR_NAME) \ + --repo-name sysext \ + --build-id $(Build.BuildId) \ + --num-clones 2 + ./bin/storm-trident helper acr \ + --remove \ + --config ${{ parameters.config }} \ + --deployment-environment ${{ parameters.deploymentEnvironment }} \ + --acr-name $(ACR_NAME) \ + --repo-name confext \ + --build-id $(Build.BuildId) \ + --num-clones 2 + + else echo "Skipping step. Configuration is '${{ parameters.config }}'." exit 0 fi - # Login to ACR - az acr login -n $(ACR_NAME) - sleep 5 - - cosi_repository_name="${{ parameters.repository }}" - build_id="$(Build.BuildId)" - az acr repository show-tags -n $(ACR_NAME) --repository $cosi_repository_name - - # Delete repository with COSI images - for i in {1..4}; do - tag="$(TAG_BASE).${i}" - if az acr repository show --name $(ACR_NAME) --image ${cosi_repository_name}:${tag} &> /dev/null; then - az acr repository delete --name $(ACR_NAME) --image ${cosi_repository_name}:${tag} --yes - fi - done - - for i in 1 2; do - tag="$(TAG_BASE).${i}" - # Delete repository with sysext images - if az acr repository show --name $(ACR_NAME) --image sysext:${tag} &> /dev/null; then - az acr repository delete --name $(ACR_NAME) --image sysext:${tag} --yes - fi - # Delete repository with confext images - if az acr repository show --name $(ACR_NAME) --image confext:${tag} &> /dev/null; then - az acr repository delete --name $(ACR_NAME) --image confext:${tag} --yes - fi - done - displayName: "Log into ACR and delete images by tag" retryCountOnTaskFailure: 3 condition: always() diff --git a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml index ad163b954..f977e7a76 100644 --- a/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml +++ b/.pipelines/templates/stages/testing_baremetal/baremetal-testing.yml @@ -165,21 +165,15 @@ stages: ${{ if eq(parameters.runtimeEnv, 'container') }}: downloadTridentContainer: true - # Push (regular or container) testimage to ACR. - # This step must occur before trident prep, since the ACR image tag - # variable is set in this template. + # Push (regular or container) testimage, sysexts, and/or confexts to + # ACR. This step must occur before trident prep, since the ACR image + # tag variable is set in this template. - template: ../common_tasks/push-to-acr.yml parameters: imageName: ${{ variables.IMAGE_NAME }} config: $(TRIDENT_CONFIGURATION_NAME) deploymentEnvironment: "bareMetal" - # Produce test sysext and confext images and push to ACR - - template: ../common_tasks/extension-images.yml - parameters: - config: ${{ variables.tridentConfigurationName }} - deploymentEnvironment: "bareMetal" - # Run trident prep - template: ../testing_common/trident-prep.yml parameters: @@ -387,3 +381,4 @@ stages: parameters: repository: ${{ variables.IMAGE_NAME }} config: $(TRIDENT_CONFIGURATION_NAME) + deploymentEnvironment: "bareMetal" diff --git a/.pipelines/templates/stages/testing_common/trident-prep.yml b/.pipelines/templates/stages/testing_common/trident-prep.yml index 8b3065376..575af20fe 100644 --- a/.pipelines/templates/stages/testing_common/trident-prep.yml +++ b/.pipelines/templates/stages/testing_common/trident-prep.yml @@ -50,14 +50,20 @@ steps: if [ "${{ parameters.config }}" == "extensions" ]; then tag="$(TAG_BASE).1" oci_sysext_url="oci://$(ACR_NAME).azurecr.io/sysext:${tag}" - oci_confext_url="oci://$(ACR_NAME).azurecr.io/confext:${tag}" cmd+=(--ociSysextUrl "$oci_sysext_url") - cmd+=(--ociConfextUrl "$oci_confext_url") # Calculate SHA384 hashes of extension images. sysext_sha384=$(sha384sum test-sysext-1.raw | awk '{print $1}') - confext_sha384=$(sha384sum test-confext-1.raw | awk '{print $1}') cmd+=(--sysextHash "$sysext_sha384") + fi + + if [ "${{ parameters.config }}" == "root-verity" ]; then + tag="$(TAG_BASE).1" + oci_confext_url="oci://$(ACR_NAME).azurecr.io/confext:${tag}" + cmd+=(--ociConfextUrl "$oci_confext_url") + + # Calculate SHA384 hashes of extension images. + confext_sha384=$(sha384sum test-confext-1.raw | awk '{print $1}') cmd+=(--confextHash "$confext_sha384") fi diff --git a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml index 0a98dbb27..b5fbf9212 100644 --- a/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml +++ b/.pipelines/templates/stages/testing_vm/netlaunch-testing.yml @@ -127,19 +127,14 @@ stages: downloadTridentContainer: ${{ variables.downloadTridentContainer }} tridentTestImageUsrVerity: ${{ variables.usrVerityTestImageName }} - # Push (regular or container) testimage to ACR + # Push (regular or container) testimages, confexts, and/or sysexts to + # ACR - template: ../common_tasks/push-to-acr.yml parameters: imageName: ${{ variables.testImageName }} config: ${{ variables.tridentConfigurationName }} deploymentEnvironment: "virtualMachine" - # Produce test sysext and confext images and push to ACR - - template: ../common_tasks/extension-images.yml - parameters: - config: ${{ variables.tridentConfigurationName }} - deploymentEnvironment: "virtualMachine" - - template: netlaunch-prep.yml - template: ../testing_common/trident-prep.yml @@ -328,10 +323,11 @@ stages: artifactsDirectory: artifacts/test-image netlistenPort: ${{variables.netlaunchPort}} - - template: ../common_tasks/remove-from-acr.yml - parameters: - repository: ${{ variables.testImageName }} - config: ${{ variables.tridentConfigurationName }} + # - template: ../common_tasks/remove-from-acr.yml + # parameters: + # repository: ${{ variables.testImageName }} + # config: ${{ variables.tridentConfigurationName }} + # deploymentEnvironment: "virtualMachine" - bash: | set -eux diff --git a/crates/trident/src/engine/clean_install.rs b/crates/trident/src/engine/clean_install.rs index fce898199..e016e65da 100644 --- a/crates/trident/src/engine/clean_install.rs +++ b/crates/trident/src/engine/clean_install.rs @@ -257,6 +257,11 @@ fn stage_clean_install( return Err(original_error).message("Failed to execute in chroot"); } + // Update the Host Configuration with information produced and stored in the + // subsystems. Currently, this step is used only to update the final paths + // of sysexts and confexts configured in the extensions subsystem. + engine::update_host_configuration(subsystems, &mut ctx)?; + // At this point, clean install has been staged, so update Host Status debug!( "Updating host's servicing state to '{:?}'", @@ -265,7 +270,7 @@ fn stage_clean_install( state.with_host_status(|hs| { *hs = HostStatus { servicing_state: ServicingState::CleanInstallStaged, - spec: host_config.clone(), + spec: ctx.spec, spec_old: Default::default(), ab_active_volume: None, partition_paths: ctx.partition_paths, diff --git a/crates/trident/src/engine/mod.rs b/crates/trident/src/engine/mod.rs index fa04989e7..41e6e21d1 100644 --- a/crates/trident/src/engine/mod.rs +++ b/crates/trident/src/engine/mod.rs @@ -22,6 +22,7 @@ use crate::{ engine::boot::BootSubsystem, subsystems::{ esp::EspSubsystem, + extensions::ExtensionsSubsystem, hooks::HooksSubsystem, initrd::InitrdSubsystem, management::ManagementSubsystem, @@ -101,6 +102,11 @@ pub(crate) trait Subsystem: Send { fn configure(&mut self, _ctx: &EngineContext) -> Result<(), TridentError> { Ok(()) } + + /// Update the Host Configuration with information from the subsystem. + fn update_host_configuration(&self, _ctx: &mut EngineContext) -> Result<(), TridentError> { + Ok(()) + } } lazy_static::lazy_static! { @@ -112,6 +118,7 @@ lazy_static::lazy_static! { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), @@ -319,6 +326,25 @@ fn configure( Ok(()) } +fn update_host_configuration( + subsystems: &[Box], + ctx: &mut EngineContext, +) -> Result<(), TridentError> { + info!("Starting step 'Update Host Configuration'"); + for subsystem in subsystems { + debug!( + "Starting step 'Update Host Configuration' for subsystem '{}'", + subsystem.name() + ); + subsystem.update_host_configuration(ctx).message(format!( + "Step 'Update Host Configuration' failed for subsystem '{}'", + subsystem.name() + ))?; + } + debug!("Finished step 'Update Host Configuration'"); + Ok(()) +} + pub fn reboot() -> Result<(), TridentError> { // Sync all writes to the filesystem. info!("Syncing filesystem"); diff --git a/crates/trident/src/engine/update.rs b/crates/trident/src/engine/update.rs index 9ce729fff..bc9ca2ec3 100644 --- a/crates/trident/src/engine/update.rs +++ b/crates/trident/src/engine/update.rs @@ -183,7 +183,7 @@ pub(crate) fn update( #[tracing::instrument(skip_all, fields(servicing_type = format!("{:?}", ctx.servicing_type)))] fn stage_update( subsystems: &mut [Box], - ctx: EngineContext, + mut ctx: EngineContext, state: &mut DataStore, #[cfg(feature = "grpc-dangerous")] sender: &mut Option< mpsc::UnboundedSender>, @@ -253,6 +253,13 @@ fn stage_update( engine::configure(subsystems, &ctx)?; }; + // Update the Host Configuration with information produced and stored in the + // subsystems. Currently, this step is used only to update the final paths + // of sysexts and confexts configured in the extensions subsystem. + engine::update_host_configuration(subsystems, &mut ctx)?; + // Turn ctx into an immutable variable. + let ctx = ctx; + // At this point, deployment has been staged, so update servicing state debug!( "Updating host's servicing state to '{:?}'", diff --git a/crates/trident/src/subsystems/extensions/mod.rs b/crates/trident/src/subsystems/extensions/mod.rs index 30d3a03af..0c8d885bd 100644 --- a/crates/trident/src/subsystems/extensions/mod.rs +++ b/crates/trident/src/subsystems/extensions/mod.rs @@ -118,6 +118,46 @@ impl Subsystem for ExtensionsSubsystem { Ok(()) } + + fn update_host_configuration(&self, ctx: &mut EngineContext) -> Result<(), TridentError> { + // Update paths of sysexts in the Host Configuration. + self.extensions + .iter() + .filter(|ext| ext.ext_type == ExtensionType::Sysext) + .try_for_each(|sysext| { + // Find corresponding sysext in Host Configuration. + ctx.spec + .os + .sysexts + .iter_mut() + .find(|ext| ext.sha384 == sysext.sha384) + .structured(InternalError::Internal( + "Failed to find previously processed sysext in Host Configuration", + ))? + .path = Some(sysext.path.clone()); + Ok::<(), TridentError>(()) + })?; + + // Update paths of confexts in the Host Configuration. + self.extensions + .iter() + .filter(|ext| ext.ext_type == ExtensionType::Confext) + .try_for_each(|confext| { + // Find corresponding confext in Host Configuration. + ctx.spec + .os + .confexts + .iter_mut() + .find(|ext| ext.sha384 == confext.sha384) + .structured(InternalError::Internal( + "Failed to find previously processed confext in Host Configuration", + ))? + .path = Some(confext.path.clone()); + Ok::<(), TridentError>(()) + })?; + + Ok(()) + } } impl ExtensionsSubsystem { @@ -470,6 +510,7 @@ mod tests { use super::*; use tempfile::TempDir; + use url::Url; #[test] fn test_populate_extensions_empty() { @@ -568,6 +609,103 @@ mod tests { assert!(mount_path.path().join("usr/lib/confexts").exists()); assert!(mount_path.path().join("usr/local/lib/confexts").exists()); } + + #[test] + fn test_update_host_configuration_sysexts() { + let mut ctx = EngineContext::default(); + ctx.spec.os.sysexts = vec![ + Extension { + url: Url::parse("https://example.com/sysext1.raw").unwrap(), + sha384: Sha384Hash::from("a".repeat(96)), + path: None, + }, + Extension { + url: Url::parse("https://example.com/sysext2.raw").unwrap(), + sha384: Sha384Hash::from("b".repeat(96)), + path: Some(PathBuf::from("/etc/extensions/sysext2.raw")), + }, + ]; + + let subsystem = ExtensionsSubsystem { + extensions: vec![ + ExtensionData { + id: "sysext1".to_string(), + name: "sysext1".to_string(), + sha384: Sha384Hash::from("a".repeat(96)), + path: PathBuf::from("/var/lib/extensions/sysext1.raw"), + temp_path: PathBuf::from(EXTENSION_IMAGE_STAGING_DIRECTORY).join("sysext1.raw"), + + ext_type: ExtensionType::Sysext, + }, + ExtensionData { + id: "sysext2".to_string(), + name: "sysext2".to_string(), + sha384: Sha384Hash::from("b".repeat(96)), + path: PathBuf::from("/etc/extensions/sysext2.raw"), + temp_path: PathBuf::from(EXTENSION_IMAGE_STAGING_DIRECTORY).join("sysext2.raw"), + + ext_type: ExtensionType::Sysext, + }, + ], + extensions_old: vec![], + }; + subsystem.update_host_configuration(&mut ctx).unwrap(); + + for i in 0..subsystem.extensions.len() { + assert_eq!( + ctx.spec.os.sysexts[i].path, + Some(subsystem.extensions[i].path.clone()) + ) + } + } + + #[test] + fn test_update_host_configuration_confexts() { + let mut ctx = EngineContext::default(); + ctx.spec.os.confexts = vec![ + Extension { + url: Url::parse("https://example.com/confext1.raw").unwrap(), + sha384: Sha384Hash::from("a".repeat(96)), + path: None, + }, + Extension { + url: Url::parse("https://example.com/confext2.raw").unwrap(), + sha384: Sha384Hash::from("b".repeat(96)), + path: Some(PathBuf::from("/usr/lib/confexts/confext2.raw")), + }, + ]; + + let subsystem = ExtensionsSubsystem { + extensions: vec![ + ExtensionData { + id: "confext1".to_string(), + name: "confext1".to_string(), + sha384: Sha384Hash::from("a".repeat(96)), + path: PathBuf::from("/var/lib/confexts/confext1.raw"), + temp_path: PathBuf::from(EXTENSION_IMAGE_STAGING_DIRECTORY) + .join("confext1.raw"), + ext_type: ExtensionType::Confext, + }, + ExtensionData { + id: "confext2".to_string(), + name: "confext2".to_string(), + sha384: Sha384Hash::from("b".repeat(96)), + path: PathBuf::from("/usr/lib/confexts/confext2.raw"), + temp_path: PathBuf::from("/var/lib/extensions/.staging/confext2.raw"), + ext_type: ExtensionType::Confext, + }, + ], + extensions_old: vec![], + }; + subsystem.update_host_configuration(&mut ctx).unwrap(); + + for i in 0..subsystem.extensions.len() { + assert_eq!( + ctx.spec.os.confexts[i].path, + Some(subsystem.extensions[i].path.clone()) + ) + } + } } #[cfg(feature = "functional-test")] diff --git a/packaging/selinux-policy-trident/trident.te b/packaging/selinux-policy-trident/trident.te index 2d981258a..f5d6d7a4e 100644 --- a/packaging/selinux-policy-trident/trident.te +++ b/packaging/selinux-policy-trident/trident.te @@ -275,8 +275,8 @@ files_var_lib_filetrans(trident_t, trident_var_lib_t, { dir file lnk_file }) # Allow trident_t domain to interact with files and directories labeled as trident_var_lib_t # Necessary so Trident can interact with the datastore at /var/lib/trident -allow trident_t trident_var_lib_t:dir { getattr search read write add_name create remove_name open mounton relabelto }; -allow trident_t trident_var_lib_t:file { getattr setattr create open read write unlink lock relabelto }; +allow trident_t trident_var_lib_t:dir { getattr search read write add_name create remove_name open mounton relabelto rmdir }; +allow trident_t trident_var_lib_t:file { getattr setattr create open read write unlink lock relabelto rename }; # Allow Trident to relabel its executable allow trident_t trident_exec_t:file relabelto; @@ -857,6 +857,9 @@ allow fsadm_t trident_t:process { siginh rlimitinh noatsecure transition sigchld allow fsadm_t fixed_disk_device_t:blk_file { open read write getattr ioctl }; allow fsadm_t unlabeled_t:file map; +# Allow fsadm_t to use losetup utility on Trident-created files in /var/lib/. Necessary to attach device to extension image. +allow fsadm_t trident_var_lib_t:file { getattr open read write }; + # Create, read, write, and delete files on a efivarfs filesystem fs_manage_efivarfs_files(fsadm_t) fs_manage_tmpfs_dirs(fsadm_t) @@ -925,6 +928,9 @@ allow udev_t cloud_init_t:fifo_file { append write getattr }; allow udev_t lvm_t:process { noatsecure rlimitinh siginh }; allow udev_t unlabeled_t:file getattr; +# Allow losetup to attach an extension image file as a loopback device. +allow udev_t trident_var_lib_t:file getattr; + files_read_generic_tmp_files(udev_t) #============= udevadm_t ============== diff --git a/tests/e2e_tests/helpers/edit_host_config.py b/tests/e2e_tests/helpers/edit_host_config.py index 7f23b11d5..197fac1d8 100644 --- a/tests/e2e_tests/helpers/edit_host_config.py +++ b/tests/e2e_tests/helpers/edit_host_config.py @@ -71,8 +71,10 @@ def rename_oci_url(host_config_path, oci_cosi_url): # Sysext and confext images are stored in ACR and tagged based on pipeline build # ID, so the HC must be updated for every build. -def add_extension_images( - host_config_path, oci_sysext_url, oci_confext_url, sysext_hash, confext_hash +def add_sysexts( + host_config_path, + oci_sysext_url, + sysext_hash, ): with open(host_config_path, "r") as f: host_config = yaml.safe_load(f) @@ -82,6 +84,19 @@ def add_extension_images( if "sysexts" not in host_config["os"]: host_config["os"]["sysexts"] = [] host_config["os"]["sysexts"].append({"url": oci_sysext_url, "sha384": sysext_hash}) + + with open(host_config_path, "w") as f: + yaml.safe_dump(host_config, f) + + +# Sysext and confext images are stored in ACR and tagged based on pipeline build +# ID, so the HC must be updated for every build. +def add_confexts(host_config_path, oci_confext_url, confext_hash): + with open(host_config_path, "r") as f: + host_config = yaml.safe_load(f) + + if "os" not in host_config: + host_config["os"] = {} if "confexts" not in host_config["os"]: host_config["os"]["confexts"] = [] host_config["os"]["confexts"].append( @@ -155,17 +170,17 @@ def main(): if args.ociCosiUrl: rename_oci_url(args.hostconfig, args.ociCosiUrl) - if ( - args.ociSysextUrl - and args.sysextHash - and args.ociConfextUrl - and args.confextHash - ): - add_extension_images( + if args.ociSysextUrl and args.sysextHash: + add_sysexts( args.hostconfig, args.ociSysextUrl, - args.ociConfextUrl, args.sysextHash, + ) + + if args.ociConfextUrl and args.confextHash: + add_confexts( + args.hostconfig, + args.ociConfextUrl, args.confextHash, ) diff --git a/tests/e2e_tests/target-configurations.yaml b/tests/e2e_tests/target-configurations.yaml index b46e24585..88509889c 100644 --- a/tests/e2e_tests/target-configurations.yaml +++ b/tests/e2e_tests/target-configurations.yaml @@ -124,6 +124,7 @@ virtualMachine: pullrequest: - base - combined + - extensions - misc - raid-mirrored - raid-resync-small diff --git a/tests/e2e_tests/trident_configurations/extensions/trident-config.yaml b/tests/e2e_tests/trident_configurations/extensions/trident-config.yaml index 6a7bdf84f..f6017e948 100644 --- a/tests/e2e_tests/trident_configurations/extensions/trident-config.yaml +++ b/tests/e2e_tests/trident_configurations/extensions/trident-config.yaml @@ -45,6 +45,16 @@ scripts: - clean-install - ab-update content: echo "testing-user ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers.d/testing-user + - name: create-override + runOn: + - clean-install + content: | + mkdir -p /etc/systemd/system/systemd-confext.service.d + cat << EOF > /etc/systemd/system/systemd-confext.service.d/override.conf + [Service] + ExecStart= + ExecStart=systemd-confext refresh --noexec=false + EOF os: selinux: mode: permissive diff --git a/tests/e2e_tests/trident_configurations/root-verity/trident-config.yaml b/tests/e2e_tests/trident_configurations/root-verity/trident-config.yaml index 95e2ddca8..e1b0da5b9 100644 --- a/tests/e2e_tests/trident_configurations/root-verity/trident-config.yaml +++ b/tests/e2e_tests/trident_configurations/root-verity/trident-config.yaml @@ -43,6 +43,12 @@ storage: - id: var type: linux-generic size: 1G + - id: exts-a + type: linux-generic + size: 1G + - id: exts-b + type: linux-generic + size: 1G - id: disk2 device: /dev/disk/by-path/pci-0000:00:1f.2-ata-3 partitionTableType: gpt @@ -61,6 +67,9 @@ storage: - id: trident-overlay volumeAId: trident-overlay-a volumeBId: trident-overlay-b + - id: exts + volumeAId: exts-a + volumeBId: exts-b verity: - id: root name: root @@ -84,6 +93,8 @@ storage: mountPoint: /var/lib/trident - deviceId: var mountPoint: /var + - deviceId: exts + mountPoint: /var/lib - deviceId: root mountPoint: path: / diff --git a/tools/go.mod b/tools/go.mod index 6a6ff51ae..d1b50e5fd 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -22,13 +22,16 @@ require ( gopkg.in/yaml.v2 v2.4.0 libvirt.org/go/libvirtxml v1.11007.0 libvirt.org/libvirt-go-xml v7.4.0+incompatible + oras.land/oras-go/v2 v2.6.0 ) require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - golang.org/x/sync v0.14.0 // indirect - oras.land/oras-go/v2 v2.6.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + golang.org/x/sync v0.16.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) require ( @@ -65,12 +68,12 @@ require ( github.com/vishvananda/netlink v1.3.0 github.com/vishvananda/netns v0.0.4 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.38.0 + golang.org/x/crypto v0.41.0 golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 - golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 ) diff --git a/tools/go.sum b/tools/go.sum index 2ca8dc8f9..7610976ee 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -54,8 +54,11 @@ github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/N github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -82,8 +85,8 @@ github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkk github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= @@ -116,8 +119,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= @@ -132,8 +135,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -149,8 +152,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -164,20 +167,20 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -185,8 +188,8 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= diff --git a/tools/storm/helpers/acr.go b/tools/storm/helpers/acr.go new file mode 100644 index 000000000..4de98c3d3 --- /dev/null +++ b/tools/storm/helpers/acr.go @@ -0,0 +1,191 @@ +package helpers + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/microsoft/storm" + "github.com/sirupsen/logrus" +) + +type AcrHelper struct { + args struct { + Push bool `help:"If set, push images to ACR" type:"bool"` + Remove bool `help:"If set, remove images from ACR" type:"bool"` + Config string `required:"" help:"Trident configuration (e.g., 'extensions')" type:"string"` + DeploymentEnvironment string `required:"" help:"Deployment environment (virtualMachine or bareMetal)" type:"string"` + AcrName string `required:"" help:"Azure Container Registry name" type:"string"` + RepoName string `required:"" help:"Repository name in ACR" type:"string"` + BuildId string `required:"" help:"Build ID" type:"string"` + FilePaths []string `help:"Array of file paths to push to ACR"` + NumClones int `help:"Number of copies of file to remove from ACR repository" type:"int"` + } +} + +func (h AcrHelper) Name() string { + return "acr" +} + +func (h *AcrHelper) Args() any { + return &h.args +} + +func (h *AcrHelper) RegisterTestCases(r storm.TestRegistrar) error { + r.RegisterTestCase("push-to-acr", h.pushToACR) + r.RegisterTestCase("remove-from-acr", h.removeFromAcr) + return nil +} + +func (h *AcrHelper) pushToACR(tc storm.TestCase) error { + if !h.args.Push { + tc.Skip("Push to ACR not requested.") + } + + // Login to ACR + err := h.loginToACR() + if err != nil { + return fmt.Errorf("failed to login to ACR: %w", err) + } + + // Push all specified files + tagBase := h.generateTagBase() + err = h.pushFiles(tagBase) + if err != nil { + return fmt.Errorf("failed to push files: %w", err) + } + + // Set output variable by writing to stdout + fmt.Printf("##vso[task.setvariable variable=TAG_BASE]%s", tagBase) + logrus.Infof("TAG_BASE set to: %s", tagBase) + + return nil +} + +func (h *AcrHelper) removeFromAcr(tc storm.TestCase) error { + if !h.args.Remove { + tc.Skip("Remove from ACR not requested.") + } + + // Login to ACR + err := h.loginToACR() + if err != nil { + return fmt.Errorf("failed to login to ACR: %w", err) + } + + tagBase := h.generateTagBase() + // Delete COSI images (for misc config) + h.deleteImagesWithTagBase(tagBase) + + logrus.Infof("Successfully completed ACR cleanup") + return nil +} + +func (h *AcrHelper) generateTagBase() string { + return fmt.Sprintf("v%s.%s.%s", h.args.BuildId, h.args.Config, h.args.DeploymentEnvironment) +} + +func (h *AcrHelper) loginToACR() error { + logrus.Infof("Logging in to ACR: %s", h.args.AcrName) + cmd := exec.Command("az", "acr", "login", "-n", h.args.AcrName) + return cmd.Run() +} + +func (h *AcrHelper) pushFiles(tagBase string) error { + for i, filePath := range h.args.FilePaths { + // Check if file exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return fmt.Errorf("file %s does not exist: %w", filePath, err) + } + + // Create tag with index + tag := fmt.Sprintf("%s.%d", tagBase, i+1) + + // Push the file + err := h.pushImage(filePath, tag) + if err != nil { + return fmt.Errorf("failed to push file %s: %w", filePath, err) + } + + // Verify the push + err = h.verifyImage(h.args.RepoName, tag) + if err != nil { + return fmt.Errorf("failed to verify %s:%s: %w", h.args.RepoName, tag, err) + } + } + + return nil +} + +func (h *AcrHelper) pushImage(filePath, tag string) error { + registryURL := fmt.Sprintf("%s.azurecr.io", h.args.AcrName) + fullImageName := fmt.Sprintf("%s/%s:%s", registryURL, h.args.RepoName, tag) + + logrus.Infof("Pushing %s with tag %s to %s", filePath, tag, registryURL) + + // Get the directory and filename from the full path + dir := filepath.Dir(filePath) + fileName := filepath.Base(filePath) + + // Use ORAS to push the image + cmd := exec.Command("oras", "push", fullImageName, fileName) + cmd.Dir = dir + err := cmd.Run() + if err != nil { + return fmt.Errorf("oras push failed for %s: %w", filePath, err) + } + + // Sleep to allow registry to process + time.Sleep(3 * time.Second) + + return nil +} + +func (h *AcrHelper) verifyImage(repository, tag string) error { + logrus.Infof("Verifying %s:%s was pushed successfully...", repository, tag) + + cmd := exec.Command("az", "acr", "repository", "show", + "--name", h.args.AcrName, + "--image", fmt.Sprintf("%s:%s", repository, tag)) + return cmd.Run() +} + +func (h *AcrHelper) deleteImagesWithTagBase(tagBase string) { + logrus.Infof("Deleting images from repository %s with tag base %s", h.args.RepoName, tagBase) + + for i := 1; i <= h.args.NumClones; i++ { + tag := fmt.Sprintf("%s.%d", tagBase, i) + err := h.deleteImageIfExists(h.args.RepoName, tag) + if err != nil { + logrus.Warnf("Failed to delete %s:%s: %v", h.args.RepoName, tag, err) + // Continue with other images even if one fails + } + } +} + +func (h *AcrHelper) deleteImageIfExists(repository, tag string) error { + // First check if the image exists + imageName := fmt.Sprintf("%s:%s", repository, tag) + checkCmd := exec.Command("az", "acr", "repository", "show", + "--name", h.args.AcrName, + "--image", imageName) + logrus.Debugf("Executing command: %s %s", checkCmd.Path, strings.Join(checkCmd.Args[1:], " ")) + err := checkCmd.Run() + if err != nil { + // Image doesn't exist, skip deletion + logrus.Debugf("Image %s/%s does not exist, skipping deletion", h.args.AcrName, imageName) + return err + } + + // Image exists, delete it + logrus.Infof("Deleting image: %s", imageName) + deleteCmd := exec.Command("az", "acr", "repository", "delete", + "--name", h.args.AcrName, + "--image", imageName, + "--yes") + logrus.Debugf("Executing command: %s %s", deleteCmd.Path, strings.Join(deleteCmd.Args[1:], " ")) + return deleteCmd.Run() +} diff --git a/tools/storm/helpers/build_extension_images.go b/tools/storm/helpers/build_extension_images.go index a7ea347c1..c21e61439 100644 --- a/tools/storm/helpers/build_extension_images.go +++ b/tools/storm/helpers/build_extension_images.go @@ -7,11 +7,14 @@ import ( "path/filepath" "github.com/microsoft/storm" + "github.com/sirupsen/logrus" ) type BuildExtensionImagesHelper struct { args struct { - NumClones int `required:"" help:"Number of sysexts and confexts to build." type:"int"` + NumClones int `help:"Number of sysexts and confexts to build." type:"int"` + BuildSysexts bool `help:"Indicates that test sysext images should be built." type:"bool"` + BuildConfexts bool `help:"Indicates that test confext images should be built." type:"bool"` } } @@ -29,14 +32,17 @@ func (h *BuildExtensionImagesHelper) RegisterTestCases(r storm.TestRegistrar) er } func (h *BuildExtensionImagesHelper) buildExtensionImages(tc storm.TestCase) error { - // Create two sysexts and confexts each - err := buildImage("sysext", h.args.NumClones) - if err != nil { - return fmt.Errorf("failed to build sysext images: %w", err) + if h.args.BuildSysexts { + err := buildImage("sysext", h.args.NumClones) + if err != nil { + return fmt.Errorf("failed to build sysext images: %w", err) + } } - err = buildImage("confext", h.args.NumClones) - if err != nil { - return fmt.Errorf("failed to build confext images: %w", err) + if h.args.BuildConfexts { + err := buildImage("confext", h.args.NumClones) + if err != nil { + return fmt.Errorf("failed to build confext images: %w", err) + } } // Verify the images were created @@ -50,10 +56,10 @@ func (h *BuildExtensionImagesHelper) buildExtensionImages(tc storm.TestCase) err if err != nil { return fmt.Errorf("failed to stat file %s: %w", file, err) } - fmt.Printf("%s %d %s\n", info.Mode(), info.Size(), file) + logrus.Infof("Built image: %s %d %s", info.Mode(), info.Size(), file) } - fmt.Println("Extension images created successfully!") + logrus.Infof("Extension images created successfully!") return nil } diff --git a/tools/storm/helpers/init.go b/tools/storm/helpers/init.go index 3b5dab4a2..1eebdd327 100644 --- a/tools/storm/helpers/init.go +++ b/tools/storm/helpers/init.go @@ -9,4 +9,5 @@ var TRIDENT_HELPERS = []storm.Helper{ &BootMetricsHelper{}, &CheckSelinuxHelper{}, &BuildExtensionImagesHelper{}, + &AcrHelper{}, }