Package Type

This document describes the design of the package resource type for managing software packages.

Overview

The package resource manages software packages with two aspects:

  • Existence: Whether the package is installed or absent
  • Version: The specific version installed (when applicable)

Provider Interface

Package providers must implement the PackageProvider interface:

type PackageProvider interface {
    model.Provider

    Install(ctx context.Context, pkg string, version string) error
    Upgrade(ctx context.Context, pkg string, version string) error
    Downgrade(ctx context.Context, pkg string, version string) error
    Uninstall(ctx context.Context, pkg string) error
    Status(ctx context.Context, pkg string) (*model.PackageState, error)
    VersionCmp(versionA, versionB string, ignoreTrailingZeroes bool) (int, error)
}

Method Responsibilities

MethodResponsibility
StatusQuery current package state (installed version or absent)
InstallInstall package at specified version (or latest if “latest”)
UpgradeUpgrade package to specified version
DowngradeDowngrade package to specified version
UninstallRemove the package
VersionCmpCompare two version strings (-1, 0, 1)

Status Response

The Status method returns a PackageState containing:

type PackageState struct {
    CommonResourceState
    Metadata *PackageMetadata
}

type PackageMetadata struct {
    Name        string         // Package name
    Version     string         // Installed version
    Arch        string         // Architecture (e.g., "x86_64")
    License     string         // Package license
    URL         string         // Package URL
    Summary     string         // Short description
    Description string         // Full description
    Provider    string         // Provider name (e.g., "dnf")
    Extended    map[string]any // Provider-specific metadata
}

The Ensure field in CommonResourceState is set to:

  • The installed version string if the package is installed
  • absent if the package is not installed

Available Providers

ProviderPackage ManagerDocumentation
dnfDNF (Fedora/RHEL)DNF
aptAPT (Debian/Ubuntu)APT

Ensure States

ValueDescription
presentPackage must be installed (any version)
absentPackage must not be installed
latestPackage must be upgraded to latest available
<version>Package must be at specific version
# Install any version
- package:
    - vim:
        ensure: present

# Install latest version
- package:
    - vim:
        ensure: latest

# Install specific version
- package:
    - nginx:
        ensure: "1.24.0-1.el9"

# Remove package
- package:
    - telnet:
        ensure: absent

Apply Logic

Phase 1: Handle Special Cases

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Get current state via Status()          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Is ensure = "latest"?                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              Yes β”‚         No
                  β–Ό         β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
    β”‚ Is package absent?  β”‚ β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
              Yes β”‚     No  β”‚
                  β–Ό     β–Ό   β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚Install β”‚ β”‚Upgrade β”‚
          β”‚latest  β”‚ β”‚latest  β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
                            β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ Is desired state met?   β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        Yes β”‚         No
                            β–Ό         β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
                    β”‚ No change β”‚     β–Ό
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  (Phase 2)

Phase 2: Handle Ensure Values

              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ What is desired ensure? β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ absent                β”‚ present               β”‚ <version>
    β–Ό                       β–Ό                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Uninstall  β”‚      β”‚ Is absent?    β”‚      β”‚ Is absent?    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
                        Yes β”‚     No           Yes β”‚     No
                            β–Ό     β–Ό                β–Ό     β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚Install β”‚ β”‚No      β”‚  β”‚Install β”‚ β”‚Compare     β”‚
                    β”‚        β”‚ β”‚change  β”‚  β”‚version β”‚ β”‚versions    β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                                                            β”‚
                                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                           β”‚ current <      β”‚ current =      β”‚ current >
                                           β–Ό                β–Ό                β–Ό
                                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                   β”‚ Upgrade   β”‚    β”‚ No change β”‚    β”‚ Downgrade β”‚
                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Version Comparison

The VersionCmp method compares two version strings:

Return ValueMeaning
-1versionA < versionB (upgrade needed)
0versionA == versionB (no change)
1versionA > versionB (downgrade needed)

Version comparison is delegated to the provider, allowing platform-specific version parsing (e.g., RPM epoch handling, Debian revision suffixes).

Idempotency

The package resource is idempotent through state comparison:

Decision Table

DesiredCurrent StateAction
ensure: presentinstalled (any version)None
ensure: presentabsentInstall
ensure: absentabsentNone
ensure: absentinstalledUninstall
ensure: latestabsentInstall latest
ensure: latestinstalledUpgrade (always runs)
ensure: <version>same versionNone
ensure: <version>older versionUpgrade
ensure: <version>newer versionDowngrade
ensure: <version>absentInstall

Special Case: ensure: latest

When ensure: latest is used:

  • The package manager determines what “latest” means
  • Upgrade is always called when the package exists (package manager is idempotent)
  • The type cannot verify if “latest” was achieved (package managers may report stale data)
  • Desired state validation only checks that the package is not absent

Package Name Validation

Package names are validated to prevent injection attacks:

Allowed Characters:

  • Alphanumeric (a-z, A-Z, 0-9)
  • Period (.), underscore (_), plus (+)
  • Colon (:), tilde (~), hyphen (-)

Rejected:

  • Shell metacharacters (;, |, &, $, etc.)
  • Whitespace
  • Quotes and backticks
  • Path separators

Version strings (when ensure is a version) are also validated for dangerous characters.

Noop Mode

In noop mode, the package type:

  1. Queries current state normally
  2. Computes version comparison
  3. Logs what actions would be taken
  4. Sets appropriate NoopMessage:
    • “Would have installed latest”
    • “Would have upgraded to latest”
    • “Would have installed version X”
    • “Would have upgraded to X”
    • “Would have downgraded to X”
    • “Would have uninstalled”
  5. Reports Changed: true if changes would occur
  6. Does not call provider Install/Upgrade/Downgrade/Uninstall methods

Desired State Validation

After applying changes (in non-noop mode), the type verifies the package reached the desired state:

func (t *Type) isDesiredState(properties, state) bool {
    switch properties.Ensure {
    case "present":
        // Any installed version is acceptable
        return state.Ensure != "absent"

    case "absent":
        return state.Ensure == "absent"

    case "latest":
        // Cannot verify "latest", just check not absent
        return state.Ensure != "absent"

    default:
        // Specific version must match
        return VersionCmp(state.Ensure, properties.Ensure, false) == 0
    }
}

If the desired state is not reached, an ErrDesiredStateFailed error is returned.