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.

Subsections of Package Type

APT Provider

This document describes the implementation details of the APT package provider for Debian-based systems.

Environment

All commands are executed with the following environment variables to ensure non-interactive operation:

VariableValuePurpose
DEBIAN_FRONTENDnoninteractivePrevents dpkg from prompting for user input
APT_LISTBUGS_FRONTENDnoneSuppresses apt-listbugs prompts
APT_LISTCHANGES_FRONTENDnoneSuppresses apt-listchanges prompts

Concurrency

A global package lock (model.PackageGlobalLock) is held during all command executions to prevent concurrent apt/dpkg operations within the same process. This prevents lock contention on /var/lib/dpkg/lock.

Operations

Status Check

Command:

dpkg-query -W -f='${Package} ${Version} ${Architecture} ${db:Status-Status}' <package>

Behavior:

  • Exit code 0 with installed status β†’ Package is present, returns version info
  • Exit code non-zero OR status not installed β†’ Package is absent

Package States: The db:Status-Status field can return various values. Only installed is treated as present:

StatusTreated AsDescription
installedPresentPackage fully installed
config-filesAbsentRemoved but config files remain
half-installedAbsentInstallation started but failed
half-configuredAbsentConfiguration failed
unpackedAbsentUnpacked but not configured
not-installedAbsentNot installed

Treating non-installed states as absent allows apt-get install to repair broken installations.

Install

Ensure Present:

apt-get install -y -q -o DPkg::Options::=--force-confold <package>

Ensure Latest:

apt-cache policy <package>                    # Get candidate version
apt-get install -y -q -o DPkg::Options::=--force-confold <package>=<version>

Specific Version:

apt-get install -y -q -o DPkg::Options::=--force-confold --allow-downgrades <package>=<version>

Flags:

FlagPurpose
-yAssume yes to prompts
-qQuiet output
-o DPkg::Options::=--force-confoldKeep existing config files on upgrade
--allow-downgradesAllow installing older versions (specific version only)

Upgrade

Delegates to Install() with the target version. The --allow-downgrades flag is only added for specific version requests, not for latest.

Downgrade

Delegates to Install() with the target version. The --allow-downgrades flag enables this operation.

Uninstall

Command:

apt-get -q -y remove <package>

Note: Uses remove not purge, so configuration files are preserved. A subsequent install will find existing config files.

Latest Available Version

Command:

apt-cache policy <package>

Parsing: Extracts the Candidate: line from output.

Example output:

zsh:
  Installed: 5.9-8+b18
  Candidate: 5.9-8+b18
  Version table:
 *** 5.9-8+b18 500
        500 http://deb.debian.org/debian trixie/main amd64 Packages
        100 /var/lib/dpkg/status

Version Comparison

Version comparison follows the Debian Policy Manual algorithm.

Version Format

[epoch:]upstream_version[-debian_revision]
ComponentRequiredDescription
epochNoInteger, default 0. Higher epoch always wins.
upstream_versionYesThe main version from upstream
debian_revisionNoDebian-specific revision

Examples:

  • 1.0 β†’ epoch=0, upstream=1.0, revision=""
  • 1:2.0-3 β†’ epoch=1, upstream=2.0, revision=3
  • 2:1.0.0+git-20190109-0ubuntu2 β†’ epoch=2, upstream=1.0.0+git-20190109, revision=0ubuntu2

Comparison Algorithm

  1. Compare epochs numerically - Higher epoch wins regardless of other components

  2. Compare upstream_version and debian_revision using the Debian string comparison:

    The string is processed left-to-right in segments:

    a. Tildes (~) - Compared first. More tildes = earlier version. Tilde sorts before everything, even empty string.

    • 1.0~alpha < 1.0 (tilde before empty)
    • 1.0~~ < 1.0~ (more tildes = earlier)

    b. Letters (A-Za-z) - Compared lexically (ASCII order)

    • Letters sort before non-letters

    c. Non-letters (., +, -) - Compared lexically

    d. Digits - Compared numerically (not lexically)

    • 9 < 13 (numeric comparison)

    These steps repeat until a difference is found or both strings are exhausted.

Comparison Examples

ABResultReason
1.02.0A < BNumeric comparison
1:1.02.0A > BEpoch 1 > epoch 0
1.0~alpha1.0A < BTilde sorts before empty
1.0~alpha1.0~betaA < BLexical: alpha < beta
1.0.11.0.2A < BNumeric: 1 < 2
1.0-11.0-2A < BRevision comparison
1.0a1.0-A < BLetters sort before non-letters

Implementation

The version comparison is implemented in version.go, ported from Puppet’s Puppet::Util::Package::Version::Debian module. It provides:

  • ParseVersion(string) - Parse a version string into components
  • CompareVersionStrings(a, b) - Compare two version strings directly
  • Version.Compare(other) - Compare parsed versions (-1, 0, 1)
  • Helper methods: LessThan, GreaterThan, Equal, etc.

DNF Provider

This document describes the implementation details of the DNF package provider for RHEL/Fedora-based systems.

Concurrency

A global package lock (model.PackageGlobalLock) is held during all command executions to prevent concurrent dnf/rpm operations within the same process. This prevents lock contention on the RPM database.

Operations

Status Check

Command:

rpm -q <package> --queryformat '%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}'

Query Format (NEVRA): The query format extracts the full NEVRA (Name, Epoch, Version, Release, Architecture):

FieldDescription
%{NAME}Package name
%|EPOCH?{%{EPOCH}}:{0}|Epoch (0 if not set)
%{VERSION}Upstream version
%{RELEASE}Release/build number
%{ARCH}Architecture (x86_64, noarch, etc.)

Example output:

zsh 0 5.8 9.el9 x86_64

Behavior:

  • Exit code 0 β†’ Package is present, parses NEVRA components
  • Exit code non-zero β†’ Package is absent

Returned Version Format: The version returned combines VERSION and RELEASE: 5.8-9.el9

The epoch and release are stored separately in the Extended metadata.

Install

Ensure Present or Latest:

dnf install -y <package>

Specific Version:

dnf install -y <package>-<version>

Flags:

FlagPurpose
-yAssume yes to all prompts

Note: DNF uses - (hyphen) to separate package name from version, unlike APT which uses =.

Upgrade

Delegates to Install(). DNF’s install command handles upgrades automatically when a newer version is available or specified.

Downgrade

Command:

dnf downgrade -y <package>-<version>

Note: Unlike upgrade, downgrade uses a dedicated DNF command rather than delegating to install.

Uninstall

Command:

dnf remove -y <package>

Version Format

RPM versions follow the EVR (Epoch:Version-Release) format:

[epoch:]version-release
ComponentRequiredDescription
epochNoInteger, default 0. Higher epoch always wins.
versionYesUpstream version number
releaseYesDistribution-specific release/build number

Examples:

  • 5.8-9.el9 β†’ epoch=0, version=5.8, release=9.el9
  • 1:2.0-3.fc39 β†’ epoch=1, version=2.0, release=3.fc39
  • 0:1.0.0-1.el9 β†’ epoch=0 (explicit), version=1.0.0, release=1.el9

Version Comparison

Version comparison uses a generic algorithm ported from Puppet, implemented in internal/util.VersionCmp().

Algorithm

The version string is tokenized into segments by splitting on -, ., digits, and non-digit sequences. Segments are compared left-to-right:

  1. Hyphens (-) - A hyphen loses to any other character
  2. Dots (.) - A dot loses to any non-hyphen character
  3. Digit sequences - Compared numerically, except:
    • Leading zeros trigger lexical comparison (01 vs 1 compared as strings)
  4. Non-digit sequences - Compared lexically (case-insensitive)

If all segments match, falls back to whole-string comparison.

Trailing Zero Normalization

When ignoreTrailingZeroes is enabled, trailing .0 segments before the first - are removed:

  • 1.0.0-rc1 β†’ 1-rc1
  • 2.0.0 β†’ 2

This allows 1.0.0 to equal 1.0 when the flag is set.

Comparison Examples

ABResultReason
1.02.0A < BNumeric: 1 < 2
1.101.9A > BNumeric: 10 > 9
1.01.0.1A < BA exhausted first
1.0-11.0-2A < BRelease comparison
1.0a1.0bA < BLexical: a < b
011A < BLeading zero: lexical comparison
1.0.01.0A > BWithout normalization
1.0.01.0A = BWith ignoreTrailingZeroes=true

Implementation

The version comparison is implemented in internal/util/util.go and provides:

  • VersionCmp(a, b, ignoreTrailingZeroes) - Compare two version strings
  • Returns -1 (a < b), 0 (a == b), or 1 (a > b)