How Aqua Manages Tool Versions: A Deep Dive into the Modular VersionGetter Pipeline
Aqua determines tool versions through a modular VersionGetter pipeline that aggregates multiple source-specific getters (GitHub tags, releases, Cargo, Go modules) and parses semantic versions using regex-based extraction.
Understanding how aquaproj/aqua resolves tool versions requires examining its pluggable architecture. The system delegates version discovery to a chain of specialized getters that handle different package registries, normalizing raw tags into clean semantic versions for installation and updates.
The VersionGetter Architecture
At the core of aqua’s version management lies the VersionGetter interface defined in [pkg/versiongetter/version_getter.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/version_getter.go). This abstraction allows the CLI to treat GitHub tags, Cargo crates, and Go modules as interchangeable version sources.
The Interface Contract
The VersionGetter interface declares two essential methods:
type VersionGetter interface {
Get(ctx context.Context, pkg *registry.PackageInfo, currentVersion string) (string, error)
List(ctx context.Context, pkg *registry.PackageInfo) ([]string, error)
}
Concrete implementations handle the specifics of querying their respective upstream APIs, while consumers remain agnostic to the underlying registry type.
The Fuzzy Finder Wrapper
Before reaching the concrete getters, version requests pass through FuzzyGetter located in [pkg/versiongetter/fuzzy_getter.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/fuzzy_getter.go). This wrapper conditionally launches a fuzzy-finder UI when useFinder is enabled, or directly delegates to the embedded VersionGetter during automated operations:
func (f *FuzzyGetter) Get(... ) string {
if !useFinder { return f.getter.Get(...) }
// … fuzzy UI logic …
}
This separation ensures that interactive and programmatic workflows share the same resolution logic.
The Version Resolution Flow
When you run aqua update or aqua install, the system triggers a coordinated lookup process across multiple files.
Controller Entry Point
The journey begins in [pkg/controller/update/update.go](https://github.com/aquaproj/aqua/blob/main/pkg/controller/update/update.go) at line 95, where the update controller invokes:
newVersion := c.fuzzyGetter.Get(ctx, logger, pkg.PackageInfo,
pkg.Package.Version, param.SelectVersion, param.Limit)
This single call initiates the entire pipeline, passing the package metadata and current version constraints down the stack.
GeneralVersionGetter Routing
The FuzzyGetter forwards requests to GeneralVersionGetter ([pkg/versiongetter/general.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/general.go)), which acts as a router. Its internal get method selects the appropriate concrete implementation based on the package's Type field:
func (g *GeneralVersionGetter) get(pkg *registry.PackageInfo) VersionGetter {
switch pkg.Type {
case "cargo": return g.cargo
case "github": // tag vs release decided later
case "go": return g.goGetter
default: return nil
}
}
This design aggregates cargo, GitHub tag, GitHub release, and Go proxy getters into a unified interface.
Concrete Getter Implementations
Each concrete getter implements registry-specific logic:
GitHubTagVersionGetter([pkg/versiongetter/github_tag.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/github_tag.go)): Queries repository tags via the GitHub APIGitHubReleaseVersionGetter([pkg/versiongetter/github_release.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/github_release.go)): Fetches release metadata instead of raw tagsCargoVersionGetter([pkg/versiongetter/cargo.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/cargo.go)): Retrieves version information from crates.ioGoGetter([pkg/versiongetter/go.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/go.go)): Resolves versions through the Go module proxy
For example, GitHubTagVersionGetter.Get lists tags, applies filters, and extracts the version using GetVersionAndPrefix (line 37 in github_tag.go).
Semantic Version Extraction
Raw tags often contain prefixes or non-standard formatting. The function GetVersionAndPrefix in [pkg/versiongetter/parse.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/parse.go) handles normalization using the regex:
^(.*?)v?((?:\d+)(?:\.\d+)?(?:\.\d+)?(?:(\.|-).+)?)$
This pattern splits strings like "release-v1.2.3" into an optional prefix (release-) and a clean semantic version (1.2.3). The implementation uses github.com/hashicorp/go-version to parse the extracted version string:
a := versionPattern.FindStringSubmatch(tag)
if a == nil { return nil, "", nil }
v, err := version.NewVersion(a[2])
Errors propagate only when a version pattern is detected but malformed, ensuring that non-version tags are silently skipped rather than causing failures.
Handling Prefixed Versions
Some ecosystems embed contextual prefixes in tags (e.g., kubernetes-1.28.0 or release-v2.0.0). Aqua preserves these prefixes during parsing so that template variables like {{.Version}} can reconstruct the original tag format when necessary. The prefix (a[1] from the regex match) travels alongside the parsed version through the entire pipeline, enabling tools that require specific tag naming conventions while still supporting semantic version comparisons.
Code Examples
Parsing a Raw Tag Directly
You can utilize the parsing utility independently to extract versions from arbitrary strings:
import (
"fmt"
"github.com/aquaproj/aqua/v2/pkg/versiongetter"
)
func main() {
version, prefix, err := versiongetter.GetVersionAndPrefix("release-v1.2.3")
if err != nil {
panic(err)
}
fmt.Printf("Version: %s, Prefix: %s\n", version, prefix)
// Output: Version: 1.2.3, Prefix: release-
}
Resolving Versions Programmatically
In a controller-like context, you interact with the getter chain as follows:
func resolveVersion(ctx context.Context, logger *slog.Logger, pkgInfo *registry.PackageInfo, curVer string) (string, error) {
// fuzzyGetter is typically injected via Wire (see pkg/controller/wire.go)
version := fuzzyGetter.Get(ctx, logger, pkgInfo, curVer, false, 0) // no UI, unlimited limit
if version == "" {
return "", fmt.Errorf("could not determine version for %s", pkgInfo.Name)
}
return version, nil
}
Dependency Injection Configuration
The concrete getter chain is wired together using Google Wire. The generated [pkg/controller/wire_gen.go](https://github.com/aquaproj/aqua/blob/main/pkg/controller/wire_gen.go) (lines 115-121) illustrates the assembly:
func InitializeController(...) (*Controller, error) {
cargoVersionGetter := versiongetter.NewCargo(client)
gitHubTagVersionGetter := versiongetter.NewGitHubTag(repositoriesService)
gitHubReleaseVersionGetter := versiongetter.NewGitHubRelease(repositoriesService)
goGetter := versiongetter.NewGoGetter(goproxyClient)
generalVersionGetter := versiongetter.NewGeneralVersionGetter(
cargoVersionGetter,
gitHubTagVersionGetter,
gitHubReleaseVersionGetter,
goGetter,
)
fuzzyGetter := versiongetter.NewFuzzy(fuzzyfinderFinder, generalVersionGetter)
// … controller assembly continues …
}
Summary
- Aqua manages tool versions through a layered
VersionGetterpipeline that abstracts multiple registry types behind a unified interface. - The
FuzzyGetterwrapper enables both interactive fuzzy-finding and automated resolution via the same underlying logic. GeneralVersionGetterroutes requests to specialized getters for Cargo, GitHub tags, GitHub releases, and Go modules based on package type.- Regex-based parsing in
GetVersionAndPrefixextracts semantic versions from raw tags while preserving optional prefixes for template rendering. - The update controller in [
pkg/controller/update/update.go](https://github.com/aquaproj/aqua/blob/main/pkg/controller/update/update.go) orchestrates the entire flow duringaqua updateoperations.
Frequently Asked Questions
What is the VersionGetter interface in Aqua?
The VersionGetter interface ([pkg/versiongetter/version_getter.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/version_getter.go)) defines the contract Get and List methods that all version sources must implement. It allows Aqua to treat GitHub repositories, Cargo crates, and Go modules interchangeably when resolving tool versions.
How does Aqua handle non-semver tags like "release-v1.2.3"?
Aqua uses the GetVersionAndPrefix function in [pkg/versiongetter/parse.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/parse.go) to split raw tags into an optional prefix and a clean semantic version using a capturing regex. The prefix is preserved for template rendering while the version component undergoes strict semantic versioning checks via hashicorp/go-version.
Which package registries does Aqua support for version lookup?
According to the source code in [pkg/versiongetter/general.go](https://github.com/aquaproj/aqua/blob/main/pkg/versiongetter/general.go), Aqua supports Cargo (crates.io), GitHub tags, GitHub releases, and Go module proxy sources. The system routes to the appropriate concrete getter based on the package's type field in the registry configuration.
How does Aqua decide between GitHub tags and releases?
When processing GitHub-based packages, the GeneralVersionGetter may utilize both GitHubTagVersionGetter and GitHubReleaseVersionGetter depending on the package configuration. The specific logic determines whether to query the tags API or the releases API, allowing packages that distribute assets exclusively through releases to resolve correctly while others may use lightweight tag-based versioning.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →