§ How-to · Specify
Use a published module
Add a published module to a root, lock it, read an upgrade before you take it and know what happens in CI and offline.
On this page
A root adds a module with one command, and pravo.lock.cue pins the version and a hash of its content. An upgrade is a diff that you read and a plan that you review.
Note
Every command below runs against tools/fakeoci, the in-memory registry in pravo's source, on port 5078, with the module from the previous guide. Modules under github.com/<owner> on ghcr.io need no CUE_REGISTRY.
Work in an empty directory. Point the shell at the registry and at a cache directory that you can find again:
export CUE_REGISTRY='example.com=localhost:5078+insecure'
export CUE_CACHE_DIR="$HOME/.cache/cue"
1. Add the dependency
module: "example.com/shop@v0"
language: {
version: "v0.17.1"
}
deps: {
"example.com/acme/pravo-service@v0": {
v: "v0.1.0"
}
}
pravo mod get pins the version in cue.mod/module.cue. When CUE_REGISTRY is not set, pravo maps every github.com/<owner> module to ghcr.io/<owner>/cue. When you set it, pravo leaves it alone.
2. Use it
Read the module's documentation for what it needs. This one builds resources through two providers that it takes as inputs, random and local. The root declares them, pins their versions and passes them in.
package shop
import (
"pravo.dev/pravo"
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
"example.com/acme/pravo-service/service"
)
$random: rnd.#Provider & {$version: "~> 3.6"}
$local: loc.#Provider & {$version: "~> 2.5"}
cart: service.#Service & {random: $random, local: $local, name: "cart", port: 8081, owner: "checkout"}
payments: service.#Service & {random: $random, local: $local, name: "payments", port: 8082, owner: "payments"}
cart_endpoint: pravo.#Output & {value: cart.endpoint}
payments_endpoint: pravo.#Output & {value: payments.endpoint}
service.#Service & {...} fills the module's inputs, and the constraints in the module check them. The module's random and local are the root's $random and $local, not copies. The resources are at the path of each instance: cart.codename, cart.config, payments.codename and payments.config.
# cart.codename (random_pet) will be created
# cart.config (local_file) will be created
# payments.codename (random_pet) will be created
# payments.config (local_file) will be created
Plan: 4 to add, 0 to change, 0 to destroy.
Graph: planned 4 of 4.
Apply complete! Resources: 4 added, 0 changed, 0 destroyed. [txn <id>]
3. Read what pravo recorded
pravo.lock.cue holds the module next to the providers.
modules: "example.com/acme/pravo-service@v0": {
version: "v0.1.0"
hash: "sha256:<hash>"
}
example.com/acme/pravo-service@v0 v0.1.0 sha256:<hash>
Verified. 1 modules match pravo.lock.cue.
The hash covers the files that pravo loaded from the module. Commit the lock file.
4. Upgrade after reading the change
The author published v0.2.0, which adds a tier input with a default. Take it.
Success! 4 resources, 0 data sources, 0 ephemeral, 0 function calls, 2 outputs across 2 providers.
CUE keeps each extracted version of a module in its cache, one directory per version. Compare the two versions of the package.
20a21,22
> // tier selects the service class.
> tier: *"standard" | "premium"
26c28
< content: "service \(name)\ncodename \(codename.id)\nport \(port)\nowner \(owner)\n"
---
> content: "service \(name)\ncodename \(codename.id)\nport \(port)\nowner \(owner)\ntier \(tier)\n"
A module is CUE, which cannot run code or reach the network, so the diff shows everything that a version can do. What it does to your infrastructure is the plan.
# cart.config (local_file) must be replaced
# payments.config (local_file) must be replaced
Plan: 2 to add, 0 to change, 2 to destroy.
Graph: planned 2 of 4, 2 unchanged and skipped, 4 refreshed.
The new input has a default, so the existing uses evaluate. The two files change and nothing else does. Apply when you are satisfied. The lock file now says v0.2.0 with the hash of that version.
5. A changed version is refused
A published version must not change. Change the hash in the lock file to simulate a republished tag or a tampered cache, and run any command.
Error: module example.com/acme/pravo-service@v0 v0.2.0 has different content than when it was locked (sha256:0000, now sha256:<hash>); a published version must not change. If this is expected, review the change and run "pravo init" to relock
Exit code 5 is the code for a refusal: a module that does not match its lock, or a plan that violates a policy. pravo mod verify lists the differences without a plan.
- example.com/acme/pravo-service@v0 v0.2.0: content differs from the lock (sha256:0000, now sha256:<hash>)
Error: 1 module(s) do not match the lock
With --json it prints the problems as data and still exits 5.
{
"ok": false,
"problems": [
"example.com/acme/pravo-service@v0 v0.2.0: content differs from the lock (sha256:0000, now sha256:<hash>)"
]
}
When nothing differs it prints "ok": true and exits 0. A pipeline can test the exit code in either mode.
Find out why the content changed before you relock. pravo init relocks.
Verified. 1 modules match pravo.lock.cue.
{
"ok": true,
"problems": []
}
6. CI and offline
pravo fetches a module when it needs it and keeps it in the CUE cache. With the registry unreachable, a warm cache loads the module. A cold cache cannot. Stop the registry with Ctrl-C in its terminal.
Success! 4 resources, 0 data sources, 0 ephemeral, 0 function calls, 2 outputs across 2 providers.
A new cache directory stands for an empty cache, as on a fresh CI runner.
Error: loading .:
example.com/shop@v0: import failed: cannot find package "example.com/acme/pravo-service/service": cannot fetch example.com/acme/pravo-service@v0.2.0: module example.com/acme/pravo-service@v0.2.0: cannot do HTTP request: Get "http://localhost:5078/v2/example.com/acme/pravo-service/manifests/v0.2.0": dial tcp [::1]:5078: connect: connection refused:
./main.cue:7:2
The exit code is 6. In CI, point CUE_CACHE_DIR at a directory that the CI system keeps between runs, or accept one fetch on the first run. Extracted modules are read-only. To clear a cache, make it writable first with chmod -R u+w.
7. When it fails
| Message | Cause and fix |
|---|---|
cannot find package "pravo.dev/providers/..." and the hint a provider schema package is missing |
The generated packages are missing, or the module needs a provider that it did not before. Run pravo init. |
cannot find module providing package example.com/acme/pravo-service/service |
The dependency is not in cue.mod/module.cue. Run pravo mod get <path>@<version>. |
has different content than when it was locked |
A published version changed. Find out why before you run pravo init. |
cannot fetch and connection refused |
The registry is unreachable and the cache is cold. |
A private module needs credentials for pulls as well as for pushes. CUE reads the container credentials that docker login writes.
What next
- Publish a module if you want to write one.
- Share code between roots in one repository when the code does not leave the repository.
- Upgrade pravo and providers covers the lock file for providers.
- pravo mod lists every subcommand.