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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions