diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 47b727ecf..af3a12830 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,60 +14,64 @@ env: jobs: - build-and-test: + build: runs-on: ubuntu-24.04 steps: - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y just pkg-config go-md2man libvirt-daemon libvirt-clients qemu-kvm qemu-system qemu-utils virtiofsd + - uses: actions/checkout@v4 - - name: Install podman for heredoc support - run: | - set -eux - echo 'deb [trusted=yes] https://ftp.debian.org/debian/ testing main' | sudo tee /etc/apt/sources.list.d/testing.list - sudo apt update - sudo apt install -y crun/testing podman/testing just + - uses: ./.github/actions/bootc-ubuntu-setup + with: + libvirt: 'true' - - name: Enable KVM group perms - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - ls -l /dev/kvm + - name: Install additional dependencies + run: sudo apt install -y go-md2man - - uses: actions/checkout@v4 + - name: Extract image lists from Justfile + run: | + echo "PRIMARY_IMAGE=$(just --evaluate PRIMARY_IMAGE)" >> $GITHUB_ENV + echo "ALL_BASE_IMAGES=$(just --evaluate ALL_BASE_IMAGES)" >> $GITHUB_ENV - name: Setup Rust uses: dtolnay/rust-toolchain@stable - - - uses: taiki-e/install-action@nextest - - name: Cache build artifacts - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@nextest - name: Build run: just validate && just build - + - name: Run unit tests run: just unit - - - name: Run integration tests - run: just test-integration - - name: Upload junit XML - if: always() + - name: Pull test images + run: just pull-test-images + + - name: Create nextest archive + run: | + cargo nextest archive --release -p integration-tests --archive-file nextest-archive.tar.zst + env: + BCVK_PATH: ${{ github.workspace }}/target/release/bcvk + BCVK_PRIMARY_IMAGE: ${{ env.PRIMARY_IMAGE }} + BCVK_ALL_IMAGES: ${{ env.ALL_BASE_IMAGES }} + + - name: Upload nextest archive uses: actions/upload-artifact@v4 with: - name: integration-junit-xml - path: target/nextest/integration/junit.xml + name: nextest-archive + path: nextest-archive.tar.zst retention-days: 7 - - name: Create archive + - name: Upload bcvk binary for tests + uses: actions/upload-artifact@v4 + with: + name: bcvk-binary-tests + path: target/release/bcvk + retention-days: 7 + + - name: Create bcvk archive run: just archive - - - name: Upload artifacts + + - name: Upload bcvk binary artifacts if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4 with: @@ -76,3 +80,81 @@ jobs: target/bcvk-*.tar.gz target/bcvk-*.tar.gz.sha256 retention-days: 7 + + integration-tests: + runs-on: ubuntu-24.04 + needs: build + strategy: + fail-fast: false + matrix: + partition: [1, 2, 3, 4] + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/bootc-ubuntu-setup + with: + libvirt: 'true' + + - name: Extract image lists from Justfile + run: | + echo "PRIMARY_IMAGE=$(just --evaluate PRIMARY_IMAGE)" >> $GITHUB_ENV + echo "ALL_BASE_IMAGES=$(just --evaluate ALL_BASE_IMAGES)" >> $GITHUB_ENV + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - uses: taiki-e/install-action@nextest + + - name: Pull test images + run: just pull-test-images + + - name: Download nextest archive + uses: actions/download-artifact@v4 + with: + name: nextest-archive + + - name: Download bcvk binary + uses: actions/download-artifact@v4 + with: + name: bcvk-binary-tests + path: target/release + + - name: Make bcvk executable + run: chmod +x target/release/bcvk + + - name: Run integration tests (partition ${{ matrix.partition }}/4) + run: | + # Clean up any leftover containers before starting + cargo run --release --bin test-cleanup -p integration-tests 2>/dev/null || true + + # Run the partitioned tests + cargo nextest run --archive-file nextest-archive.tar.zst \ + --profile integration \ + --partition hash:${{ matrix.partition }}/4 + + # Clean up containers after tests complete + cargo run --release --bin test-cleanup -p integration-tests 2>/dev/null || true + env: + BCVK_PATH: ${{ github.workspace }}/target/release/bcvk + BCVK_PRIMARY_IMAGE: ${{ env.PRIMARY_IMAGE }} + BCVK_ALL_IMAGES: ${{ env.ALL_BASE_IMAGES }} + + - name: Upload junit XML + if: always() + uses: actions/upload-artifact@v4 + with: + name: integration-junit-xml-${{ matrix.partition }} + path: target/nextest/integration/junit.xml + retention-days: 7 + + # Sentinel job for required checks - configure this job name in repository settings + required-checks: + if: always() + needs: [build, integration-tests] + runs-on: ubuntu-latest + steps: + - run: exit 1 + if: >- + needs.build.result != 'success' || + needs.integration-tests.result != 'success' diff --git a/crates/integration-tests/src/tests/libvirt_base_disks.rs b/crates/integration-tests/src/tests/libvirt_base_disks.rs index 7f09a47af..b0063a5c6 100644 --- a/crates/integration-tests/src/tests/libvirt_base_disks.rs +++ b/crates/integration-tests/src/tests/libvirt_base_disks.rs @@ -92,6 +92,41 @@ fn test_base_disk_creation_and_reuse() -> Result<()> { "Should mention using base disk" ); + // Test base-disks list shows creation timestamp + println!("Testing that base-disks list shows creation timestamp..."); + let bck = get_bck_command()?; + let list_output = std::process::Command::new(&bck) + .args(["libvirt", "base-disks", "list"]) + .output() + .expect("Failed to run base-disks list"); + + let list_stdout = String::from_utf8_lossy(&list_output.stdout); + let list_stderr = String::from_utf8_lossy(&list_output.stderr); + + if list_output.status.success() { + println!("base-disks list output:\n{}", list_stdout); + + // Should have CREATED column in header + assert!( + list_stdout.contains("CREATED"), + "Should show CREATED column in header" + ); + + // Should show timestamp values (either a date or "unknown") + // Timestamp format is YYYY-MM-DD HH:MM + let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}|unknown").unwrap(); + let has_timestamp = re.is_match(&list_stdout); + assert!( + has_timestamp, + "Should show timestamp values in CREATED column" + ); + + println!("✓ base-disks list shows creation timestamp"); + } else { + println!("base-disks list failed: {}", list_stderr); + panic!("Failed to run base-disks list: {}", list_stderr); + } + println!("✓ Base disk creation and reuse test passed"); Ok(()) } @@ -138,76 +173,6 @@ fn test_base_disks_list_command() -> Result<()> { } integration_test!(test_base_disks_list_command); -/// Test base-disks list shows creation timestamp -fn test_base_disks_list_shows_timestamp() -> Result<()> { - let test_image = get_test_image(); - let bck = get_bck_command()?; - - println!("Testing base-disks list shows creation timestamp"); - - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - let vm_name = format!("test-base-timestamp-{}", timestamp); - - cleanup_domain(&vm_name); - - // Create a VM to ensure we have at least one base disk - println!("Creating VM to generate base disk..."); - let vm_output = run_bcvk(&[ - "libvirt", - "run", - "--name", - &vm_name, - "--filesystem", - "ext4", - &test_image, - ])?; - - if !vm_output.success() { - cleanup_domain(&vm_name); - panic!("Failed to create VM: {}", vm_output.stderr); - } - - // Run base-disks list - let output = Command::new(&bck) - .args(["libvirt", "base-disks", "list"]) - .output() - .expect("Failed to run base-disks list"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - - cleanup_domain(&vm_name); - - if output.status.success() { - println!("base-disks list output:\n{}", stdout); - - // Should have CREATED column in header - assert!( - stdout.contains("CREATED"), - "Should show CREATED column in header" - ); - - // Should show timestamp values (either a date or "unknown") - // Timestamp format is YYYY-MM-DD HH:MM - let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}|unknown").unwrap(); - let has_timestamp = re.is_match(&stdout); - assert!( - has_timestamp, - "Should show timestamp values in CREATED column" - ); - - println!("✓ base-disks list shows creation timestamp"); - } else { - println!("base-disks list failed: {}", stderr); - panic!("Failed to run base-disks list: {}", stderr); - } - Ok(()) -} -integration_test!(test_base_disks_list_shows_timestamp); - /// Test base-disks prune command with dry-run fn test_base_disks_prune_dry_run() -> Result<()> { let bck = get_bck_command()?; diff --git a/crates/integration-tests/src/tests/libvirt_verb.rs b/crates/integration-tests/src/tests/libvirt_verb.rs index 0fa43cea7..421b40189 100644 --- a/crates/integration-tests/src/tests/libvirt_verb.rs +++ b/crates/integration-tests/src/tests/libvirt_verb.rs @@ -88,142 +88,6 @@ fn test_libvirt_list_json_output() -> Result<()> { } integration_test!(test_libvirt_list_json_output); -/// Test libvirt list JSON output includes SSH metadata -fn test_libvirt_run_list_json_ssh_metadata() -> Result<()> { - let test_image = get_test_image(); - - // Generate unique domain name for this test - let domain_name = format!( - "test-json-ssh-{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - ); - - println!( - "Testing libvirt list JSON output with SSH metadata for domain: {}", - domain_name - ); - - // Cleanup any existing domain with this name - cleanup_domain(&domain_name); - - // Create domain with SSH key generation (default behavior) - println!("Creating libvirt domain with SSH key..."); - let create_output = run_bcvk(&[ - "libvirt", - "run", - "--name", - &domain_name, - "--label", - LIBVIRT_INTEGRATION_TEST_LABEL, - "--filesystem", - "ext4", - &test_image, - ]) - .expect("Failed to run libvirt run"); - - println!("Create stdout: {}", create_output.stdout); - println!("Create stderr: {}", create_output.stderr); - - if !create_output.success() { - cleanup_domain(&domain_name); - panic!("Failed to create domain with SSH: {}", create_output.stderr); - } - - println!("Successfully created domain: {}", domain_name); - - // List domains with JSON format - println!("Listing domains with JSON format..."); - let bck = get_bck_command()?; - let list_output = Command::new(&bck) - .args(["libvirt", "list", "--format", "json", "-a"]) - .output() - .expect("Failed to run libvirt list --format json"); - - let list_stdout = String::from_utf8_lossy(&list_output.stdout); - println!("List JSON output: {}", list_stdout); - - // Cleanup domain before assertions - cleanup_domain(&domain_name); - - // Check that the command succeeded - if !list_output.status.success() { - let stderr = String::from_utf8_lossy(&list_output.stderr); - panic!("libvirt list --format json failed: {}", stderr); - } - - // Parse JSON output - let domains: Vec = - serde_json::from_str(&list_stdout).expect("Failed to parse JSON output from libvirt list"); - - // Find our test domain in the output - let test_domain = domains - .iter() - .find(|d| d["name"].as_str() == Some(&domain_name)) - .expect(&format!( - "Test domain '{}' not found in JSON output", - domain_name - )); - - println!("Found test domain in JSON output: {:?}", test_domain); - - // Verify SSH port is present and is a number - let ssh_port = test_domain["ssh_port"] - .as_u64() - .expect("ssh_port should be present and be a number"); - assert!( - ssh_port > 0 && ssh_port < 65536, - "ssh_port should be a valid port number, got: {}", - ssh_port - ); - println!("✓ ssh_port is present and valid: {}", ssh_port); - - // Verify has_ssh_key is true - let has_ssh_key = test_domain["has_ssh_key"] - .as_bool() - .expect("has_ssh_key should be present and be a boolean"); - assert!( - has_ssh_key, - "has_ssh_key should be true for domain created with SSH key" - ); - println!("✓ has_ssh_key is true"); - - // Verify ssh_private_key is present and looks like a valid SSH key - let ssh_private_key = test_domain["ssh_private_key"] - .as_str() - .expect("ssh_private_key should be present and be a string"); - assert!( - !ssh_private_key.is_empty(), - "ssh_private_key should not be empty" - ); - assert!( - ssh_private_key.contains("-----BEGIN") && ssh_private_key.contains("PRIVATE KEY-----"), - "ssh_private_key should be a valid SSH private key format, got: {}", - &ssh_private_key[..std::cmp::min(100, ssh_private_key.len())] - ); - assert!( - ssh_private_key.contains("-----END") && ssh_private_key.contains("PRIVATE KEY-----"), - "ssh_private_key should have proper end marker" - ); - - // Verify the key has proper newlines (not escaped \n) - assert!( - ssh_private_key.lines().count() > 1, - "ssh_private_key should have multiple lines, not escaped newlines" - ); - - println!( - "✓ ssh_private_key is present and valid (has {} lines)", - ssh_private_key.lines().count() - ); - - println!("✓ libvirt list JSON SSH metadata test passed"); - Ok(()) -} -integration_test!(test_libvirt_run_list_json_ssh_metadata); - /// Test domain resource configuration options fn test_libvirt_run_resource_options() -> Result<()> { let bck = get_bck_command()?; @@ -340,13 +204,15 @@ fn test_libvirt_ssh_integration() -> Result<()> { } integration_test!(test_libvirt_ssh_integration); -/// Test libvirt run with instancetype -fn test_libvirt_run_with_instancetype() -> Result<()> { +/// Comprehensive workflow test: creates a VM and tests multiple features +/// This consolidates several smaller tests to reduce expensive disk image creation +fn test_libvirt_comprehensive_workflow() -> Result<()> { let test_image = get_test_image(); + let bck = get_bck_command()?; // Generate unique domain name for this test let domain_name = format!( - "test-itype-{}", + "test-workflow-{}", std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() @@ -354,15 +220,15 @@ fn test_libvirt_run_with_instancetype() -> Result<()> { ); println!( - "Testing libvirt run with instancetype for domain: {}", + "Testing comprehensive libvirt workflow for domain: {}", domain_name ); // Cleanup any existing domain with this name cleanup_domain(&domain_name); - // Create domain with instancetype - println!("Creating libvirt domain with instancetype u1.small..."); + // Create domain with multiple features: instancetype, labels, SSH + println!("Creating libvirt domain with instancetype and labels..."); let create_output = run_bcvk(&[ "libvirt", "run", @@ -370,6 +236,8 @@ fn test_libvirt_run_with_instancetype() -> Result<()> { &domain_name, "--label", LIBVIRT_INTEGRATION_TEST_LABEL, + "--label", + "test-workflow", "--itype", "u1.small", "--filesystem", @@ -383,31 +251,24 @@ fn test_libvirt_run_with_instancetype() -> Result<()> { if !create_output.success() { cleanup_domain(&domain_name); - panic!( - "Failed to create domain with instancetype: {}", - create_output.stderr - ); + panic!("Failed to create domain: {}", create_output.stderr); } println!("Successfully created domain: {}", domain_name); - // Inspect the domain to verify instancetype was set + // Test 1: Verify instancetype configuration (u1.small: 1 vcpu, 2048 MB) + println!("Test 1: Verifying instancetype configuration..."); let inspect_output = run_bcvk(&["libvirt", "inspect", "--format", "xml", &domain_name]) .expect("Failed to run libvirt inspect"); let inspect_stdout = inspect_output.stdout; - println!("Inspect output: {}", inspect_stdout); - - // Parse XML to verify memory and vcpus match u1.small (1 vcpu, 2048 MB) let dom = parse_xml_dom(&inspect_stdout).expect("Failed to parse domain XML"); - // Check vCPUs (should be 1 for u1.small) let vcpu_node = dom.find("vcpu").expect("vcpu element not found"); let vcpus: u32 = vcpu_node.text.parse().expect("Failed to parse vcpu count"); assert_eq!(vcpus, 1, "u1.small should have 1 vCPU, got {}", vcpus); println!("✓ vCPUs correctly set to: {}", vcpus); - // Check memory (should be 2048 MB = 2097152 KB for u1.small) let memory_node = dom.find("memory").expect("memory element not found"); let memory_kb: u64 = memory_node.text.parse().expect("Failed to parse memory"); let memory_mb = memory_kb / 1024; @@ -418,13 +279,118 @@ fn test_libvirt_run_with_instancetype() -> Result<()> { ); println!("✓ Memory correctly set to: {} MB", memory_mb); + // Test 2: Verify labels in domain XML + println!("Test 2: Verifying label functionality..."); + let dumpxml_output = Command::new("virsh") + .args(&["dumpxml", &domain_name]) + .output() + .expect("Failed to dump domain XML"); + + let domain_xml = String::from_utf8_lossy(&dumpxml_output.stdout); + + assert!( + domain_xml.contains("bootc:label") || domain_xml.contains("