§ Reference · Language
Reuse
pravo has no module block. A definition that contains resources is instantiated with &, a package shares it between roots and a CUE module between repositories.
On this page
A module block, a source and a providers map have no counterpart in pravo. The unit of reuse is a definition that contains resources, or a package of such definitions. Its resources are written as in a root. The instance sits at a path, and that path is the prefix of the addresses of the resources inside. There is nothing to wire.
The word module names one thing in pravo: a CUE module, the tree of packages under cue.mod/module.cue. Import paths start with its name, and pravo mod publishes it to a registry, fetches it by version and locks its content. A definition travels between repositories inside one.
| Form | Written as | Resource address |
|---|---|---|
| Definition in the root | api: #Service & {name: "api"} |
api.secret |
| Several instances | services: {for n, p in ports {"\(n)": #Service & {...}}} |
services.web.secret |
| Package in the same CUE module | api: svc.#Service & {...} |
api.secret |
| Package from a published CUE module | api: svc.#Service & {...}, after pravo mod get |
api.secret |
| Package with a constraint | $eu: strict.#Random |
the addresses of the resources built through $eu |
Inputs are the fields of the definition. A required input is a field with no default. Constraints on the fields do the work of validation. Two resources whose addresses differ only in case, such as Logs and logs, are an error: a state on a file system that ignores case would keep both in one file.
Publishing, locking and fetching CUE modules are commands: see mod, Publish a CUE module, Use a published CUE module and Share code between roots in one repository.
Definitions
Work in an empty directory. Write cue.mod/module.cue first: a root is inside a CUE module, and pravo init creates the file when none exists.
module: "example.com/infra@v0"
language: version: "v0.14.0"
package infra
import (
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
)
$random: rnd.#Provider
$local: loc.#Provider
#Service: {
name: string
port: int & >1024
secret: $random.#Resource.random_password & {length: 24, special: false}
config: $local.#Resource.local_file & {
filename: "out/\(name).json"
content: "{\"port\": \(port)}"
}
}
api: #Service & {name: "api", port: 8080}
services: {
for n, p in {web: 3000, worker: 3001} {
"\(n)": #Service & {name: n, port: p}
}
}
# api.secret (random_password) will be created
# api.config (local_file) will be created
# services.web.secret (random_password) will be created
# services.web.config (local_file) will be created
# services.worker.secret (random_password) will be created
# services.worker.config (local_file) will be created
Plan: 6 to add, 0 to change, 0 to destroy.
The resources inside are built through the providers of the package, here $random and $local. A reference such as api.config.filename is a reference to the resource.
A constraint on an input is checked when the specification is evaluated:
package infra
#Service: {
name: string
port: int & >1024
}
api: #Service & {name: "api", port: 80}
Error: evaluating .:
api.port: invalid value 80 (out of bound >1024):
./main.cue:5:14
./main.cue:8:37
A required input that is left out stays non-concrete. The plan stops at the first value that needs it:
package infra
import loc "pravo.dev/providers/hashicorp/local"
$local: loc.#Provider
#Service: {
name: string
config: $local.#Resource.local_file & {
filename: "out/\(name).json"
content: "{}"
}
}
api: #Service
Error: api.config.filename: value depends on #Service.name, which is not concrete (is a -t tag missing?)
Name the loop variable differently from the input it fills. In {name: n, port: port} the right-hand port is the field itself, which is never concrete.
Packages
To share a definition between roots, put it in a package and import it. The root must be inside a CUE module. A package cannot see the providers of the root that imports it, so it takes them as inputs and builds its resources through them.
package svc
import (
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
)
#Service: {
name: string
random!: rnd.#Provider
local!: loc.#Provider
secret: random.#Resource.random_password & {length: 16}
config: local.#Resource.local_file & {
filename: "\(name).txt"
content: secret.result
}
}
package infra
import (
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
"example.com/infra/svc"
)
$random: rnd.#Provider & {$version: "~> 3.6"}
$local: loc.#Provider
api: svc.#Service & {name: "api", random: $random, local: $local}
# api.secret (random_password) will be created
# api.config (local_file) will be created
Plan: 2 to add, 0 to change, 0 to destroy.
Providers in a shared definition
A definition that is published or reused declares each provider it needs as a required input of type #Provider, provider!: loc.#Provider, and builds its resources through it. The caller passes a provider struct. The input is an alias of that struct, not a second instance: the resource is recorded under the provider instance of the caller. There is no line on the resource that names the provider. Building it through provider is enough.
A definition with several providers declares one input for each, and the caller passes them all, as random: $random, local: $local above. A definition written in the same package as its provider may refer to a top-level provider directly. That ties it to the package, so a definition meant for other roots takes its providers as inputs.
package infra
import loc "pravo.dev/providers/hashicorp/local"
#Note: {
provider!: loc.#Provider
text: string
file: provider.#Resource.local_file & {
filename: "out/\(text).txt"
content: "\(text)\n"
}
}
$primary: loc.#Provider & {$version: "~> 2.5"}
$secondary: loc.#Provider
one: #Note & {provider: $primary, text: "one"}
two: #Note & {provider: $secondary, text: "two"}
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <id>]
{
"one.file": "local.$primary"
"two.file": "local.$secondary"
}
Destroy complete! Resources: 0 added, 0 changed, 2 destroyed. [txn <id>]
An instance that passes no provider
A required input that an instance leaves out is an error when the specification loads. The message names the instance and the field. No provider starts, and no resource is built through a provider that nobody configured.
package infra
import loc "pravo.dev/providers/hashicorp/local"
#Note: {
provider!: loc.#Provider
text: string
file: provider.#Resource.local_file & {
filename: "out/\(text).txt"
content: "\(text)\n"
}
}
$primary: loc.#Provider
one: #Note & {provider: $primary, text: "one"}
two: #Note & {text: "two"}
Error: provider two.provider was not supplied: two requires a hashicorp/local provider in its field provider. Pass one, for example
two: ... & {provider: $local}
A field without the ! is a provider struct of its own. An instance that leaves it out builds its resources through that struct, a second instance of the provider with no configuration, and nothing warns. Pitfalls shows the run. A required input has no such default.
Configuration in the definition and in the caller
The instance of a passed-in provider is configured from the caller's struct. The input is another name for that struct, so what the definition writes on it is checked against what the caller wrote.
| The definition writes | The caller's struct | Result |
|---|---|---|
| nothing | any | The caller's configuration. |
| a value | the same value | The caller's configuration. |
| a value | another value for the same field | CUE reports conflicting values, with the path of the field in the instance. |
| a value | nothing for that field | An error that names the field. The definition cannot configure a provider it was given. |
Neither side wins a conflict: the specification does not load. The provider tls takes its proxy in configuration, so it can show both errors.
package infra
import tlsp "pravo.dev/providers/hashicorp/tls"
#Key: {
tls!: tlsp.#Provider & {proxy: [{url: "http://shared.internal:3128"}]}
key: tls.#Resource.tls_private_key & {algorithm: "ECDSA", ecdsa_curve: "P256"}
}
$tls: tlsp.#Provider & {proxy: [{url: "http://caller.internal:3128"}]}
a: #Key & {tls: $tls}
Error: evaluating .:
a.tls.proxy.0.url: conflicting values "http://caller.internal:3128" and "http://shared.internal:3128":
./main.cue:6:40
./main.cue:10:39
./main.cue:12:4
./main.cue:12:17
When the caller's struct leaves the field out, nothing conflicts, and the value would never reach the provider. pravo reports an error.
package infra
import tlsp "pravo.dev/providers/hashicorp/tls"
#Key: {
tls!: tlsp.#Provider & {proxy: [{url: "http://shared.internal:3128"}]}
key: tls.#Resource.tls_private_key & {algorithm: "ECDSA", ecdsa_curve: "P256"}
}
$tls: tlsp.#Provider
a: #Key & {tls: $tls}
Error: a.tls sets proxy, but a.tls is another name for the provider $tls, which does not set it. The provider is configured from $tls alone: set proxy there
The same rule holds for the $ fields, such as $version, and for a provider struct that is built from another one at the top level, such as $other: $tls & {proxy: [...]}. Configure a provider where it is declared.
Constraints in a package
Unification is commutative, so a package can constrain resources it did not create. A team ships a provider definition that fixes what the resources built through it may look like, and each root builds its provider from it. The rule is a constraint, checked when the specification evaluates. It is not a policy, which judges a plan.
package strict
import rnd "pravo.dev/providers/hashicorp/random"
#Random: rnd.#Provider & {
#Resource: random_pet: prefix: =~"^eu-"
}
package infra
import "example.com/infra/strict"
$eu: strict.#Random
a: $eu.#Resource.random_pet & {length: 2, prefix: "us-"}
Error: evaluating .:
a.prefix: invalid value "us-" (out of bound =~"^eu-"):
./strict/strict.cue:6:33
./cue.mod/gen/pravo.dev/providers/hashicorp/random/random_gen.cue:238:12
./main.cue:7:51
With prefix: "eu-" the same root validates.
A constraint rejects a value that is set and wrong. It does not reject an attribute that is left out. An unset attribute with no reference to another resource is sent to the provider as null. A rule that an attribute is present is a rule for a policy. See Providers and Policies.
See also
- Share code between roots in one repository, Publish a CUE module and Use a published CUE module cover the three ways to share code.
pravo modlists the commands for CUE modules.- Providers describes the provider input.