Skip to content

Implement Installation Logic #117

@josecelano

Description

@josecelano

Implement Installation Logic (Issue 1-1-4)

Overview

Implement the installation logic for all 4 dependencies (cargo-machete, OpenTofu, Ansible, LXD) and add the install command to the CLI. This completes the dependency installer package by converting bash installation scripts to Rust with structured logging for automation and CI/CD integration.

Design Philosophy: Uses structured logging only (tracing crate) - no user-facing println!() output. Designed for automation and CI/CD pipelines.

Parent Issue

#113 - Create Dependency Installation Package for E2E Tests

Dependencies

Depends On:

  • #116 - Create Docker Test Infrastructure (Issue 1-1-3)

Blocks:

  • Issue 1-2: Integrate dependency-installer with E2E tests

Objectives

  • Define DependencyInstaller trait for installation abstraction
  • Convert bash scripts (scripts/setup/) to Rust installer implementations
  • Add install command to CLI binary with handler-based architecture
  • Extend DependencyManager to coordinate installation
  • Extend Docker tests to verify actual installations
  • Use structured logging (tracing) throughout for observability
  • Document exit codes and usage patterns

Key Components

DependencyInstaller Trait

#[async_trait]
pub trait DependencyInstaller: Send + Sync {
    fn name(&self) -> &str;
    fn dependency(&self) -> Dependency;
    async fn install(&self) -> Result<(), InstallationError>;
    fn requires_sudo(&self) -> bool { false }
}

Installer Implementations

Convert bash scripts to Rust:

  1. CargoMacheteInstaller (scripts/setup/install-cargo-machete.sh)

    • Uses cargo install cargo-machete
    • No sudo required
  2. OpenTofuInstaller (scripts/setup/install-opentofu.sh)

    • Downloads installer script with curl
    • Runs with sudo
    • Multi-step: download → chmod → execute → cleanup
  3. AnsibleInstaller (scripts/setup/install-ansible.sh)

    • Uses apt-get package manager
    • Requires sudo
  4. LxdInstaller (scripts/setup/install-lxd.sh)

    • Uses snap for installation
    • Configures user groups
    • Requires sudo

Extended DependencyManager

Add installation methods to existing manager:

impl DependencyManager {
    pub fn get_installer(&self, dep: Dependency) -> Box<dyn DependencyInstaller>;
    pub async fn install(&self, dep: Dependency) -> Result<(), InstallationError>;
    pub async fn install_all(&self) -> Result<Vec<InstallResult>, InstallationError>;
}

Install Command Handler

New handler following existing pattern:

// src/handlers/install.rs
pub async fn handle_install(
    manager: &DependencyManager,
    dependency: Option<Dependency>,
) -> Result<(), InstallError> {
    // Handler implementation with structured logging
}

CLI Command

Add to existing CLI:

# Install all dependencies
dependency-installer install

# Install specific dependency
dependency-installer install --dependency opentofu

# With verbose logging
dependency-installer install --verbose

Docker Tests

Extend testing to verify actual installations:

// tests/docker_install_command.rs
#[tokio::test]
async fn test_install_cargo_machete() {
    // Verify installation in clean container
}

#[tokio::test]
async fn test_install_idempotent() {
    // Install twice, both should succeed
}

#[tokio::test]
async fn test_install_all() {
    // Install all dependencies
}

Architecture

Directory Structure

packages/dependency-installer/
├── src/
│   ├── manager.rs            # Add installation methods
│   ├── detector/             # Existing detection logic
│   ├── installer/            # NEW: Installation logic
│   │   ├── mod.rs            # Trait + error types
│   │   ├── cargo_machete.rs
│   │   ├── opentofu.rs
│   │   ├── ansible.rs
│   │   └── lxd.rs
│   ├── handlers/             # Extend with install
│   │   ├── check.rs          # Existing
│   │   ├── list.rs           # Existing
│   │   └── install.rs        # NEW
│   └── cli.rs                # Add Install command
└── tests/
    └── docker_install_command.rs  # NEW tests

Handler-Based Architecture

Following existing pattern:

// src/app.rs
match cli.command {
    Commands::Check { dependency } => {
        check::handle_check(&manager, dependency)?;
    }
    Commands::List => {
        list::handle_list(&manager)?;
    }
    Commands::Install { dependency } => {  // NEW
        install::handle_install(&manager, dependency).await?;
    }
}

Structured Logging Examples

All output uses tracing crate - no println!() statements:

Installing All Dependencies

$ dependency-installer install
2025-11-04T10:15:20Z  INFO install: Installing all dependencies
2025-11-04T10:15:21Z  INFO install: Installing dependency dependency="cargo-machete"
2025-11-04T10:15:25Z  INFO install: Installation successful dependency="cargo-machete" status="installed"
2025-11-04T10:15:26Z  INFO install: Installing dependency dependency="OpenTofu"
2025-11-04T10:15:35Z  INFO install: Installation successful dependency="OpenTofu" status="installed"
...
2025-11-04T10:15:56Z  INFO install: All dependencies installed successfully

Installing Specific Dependency with Verbose Logging

$ dependency-installer install --dependency opentofu --verbose
2025-11-04T10:25:10Z  INFO install: Installing specific dependency dependency="opentofu"
2025-11-04T10:25:11Z DEBUG opentofu_installer: Downloading installer script
2025-11-04T10:25:13Z DEBUG opentofu_installer: Making script executable
2025-11-04T10:25:14Z DEBUG opentofu_installer: Running installer with sudo
2025-11-04T10:25:20Z DEBUG opentofu_installer: Cleaning up installer script
2025-11-04T10:25:20Z  INFO install: Installation complete dependency="opentofu" status="installed"

Controlling Log Output

# Default (INFO and above)
dependency-installer install

# Verbose (DEBUG and above)
dependency-installer install --verbose

# Specific level
dependency-installer install --log-level trace

# Environment variable
RUST_LOG=debug dependency-installer install

Exit Codes

The CLI uses consistent exit codes for automation:

  • 0: Success (all installations succeeded)
  • 1: Installation failures (one or more dependencies failed)
  • 2: Invalid arguments (e.g., unknown dependency name)
  • 3: Internal error (unexpected failure)

Example:

$ dependency-installer install --dependency opentofu
$ echo $?
0  # Success

$ dependency-installer install --dependency nonexistent
Error: Invalid value 'nonexistent' for '--dependency <DEPENDENCY>'
$ echo $?
2  # Invalid argument

Implementation Tasks

Phase 1: Installer Trait and Error Types

  • Create src/installer/mod.rs
  • Define DependencyInstaller trait with async methods
  • Define InstallationError enum with thiserror
  • Add module exports

Phase 2: Convert Bash Scripts to Rust

  • Cargo-machete: Create src/installer/cargo_machete.rs
  • OpenTofu: Create src/installer/opentofu.rs (multi-step with curl)
  • Ansible: Create src/installer/ansible.rs (apt-get with sudo)
  • LXD: Create src/installer/lxd.rs (snap with groups)
  • Add structured logging to all installers
  • Handle errors with InstallationError

Phase 3: Update DependencyManager

  • Add get_installer() method
  • Implement install() async method
  • Implement install_all() async method
  • Define InstallResult struct

Phase 4: Add Install Command Handler

  • Create src/handlers/install.rs
  • Implement handle_install() function
  • Implement helper functions
  • Define handler error types
  • Add structured logging
  • Update src/handlers/mod.rs

Phase 5: Update CLI and App

  • Update src/cli.rs with Install command
  • Update src/app.rs to handle install command
  • Ensure async support configured
  • Update help text

Phase 6: Docker Test Infrastructure

  • Update tests/containers/ubuntu.rs with sudo support
  • Create tests/docker_install_command.rs
  • Implement tests for each dependency
  • Implement idempotency tests
  • Implement install-all test

Phase 7: Testing and Validation

  • Unit tests for installers
  • Integration tests in Docker
  • Manual testing
  • Run complete test suite
  • Pre-commit checks pass

Phase 8: Documentation

  • Update packages/dependency-installer/README.md
  • Document installer implementations
  • Document exit codes
  • Add usage examples

Acceptance Criteria

DependencyInstaller Trait:

  • Trait defined with clear contract
  • All 4 installers implement the trait
  • Error handling uses InstallationError consistently
  • Structured logging provides observability

Installer Implementations:

  • All installers work in Docker containers
  • Installations are idempotent (can run multiple times)
  • Sudo requirements correctly marked
  • Verified with check command
  • Clear, actionable error messages

CLI Install Command:

  • Works for all dependencies individually
  • Works for installing all dependencies
  • Structured logging output is informative
  • Exit codes are correct
  • Help text is accurate

Docker Tests:

  • Tests verify actual installation in clean containers
  • Idempotency tests pass
  • Verification with check command works
  • All tests pass consistently

Quality:

  • Pre-commit checks pass
  • No clippy warnings
  • Code properly formatted
  • Documentation complete

Related Documentation

Notes

  • Time Estimate: 4-5 hours (largest of the 4 phases)
  • Design Pattern: Two-trait design (DependencyDetector + DependencyInstaller)
  • Automation Focus: Structured logging only, no user interaction
  • Idempotent: All installers handle repeated runs safely
  • Docker Testing: Required to ensure installations work in clean environments

Next Steps After Completion

  1. Dependency-installer package is complete
  2. Issue 1-2: Integrate with E2E tests
  3. Issue 1-3: Update CI workflows to use binary instead of bash scripts

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions