§ B1 · Track B · pravo
Providers and resources
A provider is a struct built from a generated definition. A resource is a struct built through it. Its address is its path.
On this page
Warm-up
Two questions from Track A. Decide, then open the answer.
Predict A definition #Bucket declares the field name. You build logs: #Bucket & {nmae: "logs"}. Is it accepted?
No. A definition is closed, so nmae is field not allowed (A8).
Predict A file says only size: int. Does pravo eval -c accept it?
No. int is a type and not a value, and -c demands a concrete value (A2).
The problem
You want to know what a resource sends to its provider before anything runs. In HCL the answer is a plan. A plan starts the provider, needs the credentials of a cloud provider and reads the real objects.
The block gives no other way to look. A resource is a block with two labels, and the labels make its address, here random_integer.port. Other code cannot pass a block around, merge it with another block or constrain it.
The idea
In CUE there are no blocks. A resource is a struct, and pravo recognises it by what the struct was built from. A struct is a value, so pravo can print it before any provider starts. The same intent in both languages:
resource "random_integer" "port" {
min = 20000
max = 29999
seed = "web"
}
resource "local_file" "web" {
filename = "web.conf"
content = "port = ${random_integer.port.result}\n"
}
$random: rnd.#Provider
$local: loc.#Provider
port: $random.#Resource.random_integer & {
min: 20000
max: 29999
seed: "web"
}
web: $local.#Resource.local_file & {
filename: "web.conf"
content: "port = \(port.result)\n"
}
A provider is a struct built from #Provider, a definition that pravo init generates. A resource is a struct built through a provider. Its address is its path in the file. A reference to another resource is the dependency.
This lesson uses two providers that run on your machine, hashicorp/random and hashicorp/local.
Write the specification
Work in an empty directory. Save this as main.cue. pravo reads every .cue file of the package in the directory.
package estate
import (
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
)
$random: rnd.#Provider & {$version: "~> 3.6"}
$local: loc.#Provider
port: $random.#Resource.random_integer & {
min: 20000
max: 29999
seed: "web"
}
web: $local.#Resource.local_file & {
filename: "web.conf"
content: "port = \(port.result)\n"
}
Read it from the top.
rndandlocare packages thatpravo initgenerates, one per provider. They do not exist yet.$randomis a provider struct.$versionpins the provider and is not sent to it.portis a resource.$random.#Resource.random_integerselects its type from the provider. Theseedmakes the number repeatable, so your output matches this page.webreadsport.result. That reference is the dependency, so pravo createsportfirst. There is nodepends_onto write.
Install the providers
Configuration does not load yet; generating schemas for the declared providers first.
Created cue.mod/module.cue
Providers:
...
pravo has been initialised. Run "pravo plan" to see what it would do.
The lines in the middle name the provider versions that were chosen. The first line says that the file could not load before the packages existed. init read the imports, installed the providers, and wrote the packages.
cue.mod
main.cue
pravo.lock.cue
cue.mod/gen/pravo.dev/pravo/settings_gen.cue
cue.mod/gen/pravo.dev/providers/hashicorp/local/local_gen.cue
cue.mod/gen/pravo.dev/providers/hashicorp/random/random_gen.cue
cue.mod/module.cue
pravo.lock.cue pins the provider versions. Commit it. The files under cue.mod/gen are generated. Do not edit them. The file of a provider defines #Provider, and the types of the provider sit inside it:
#Lifecycle: {
#Provider: {
#Resource: {
#Data: {
#Ephemeral: {
#Function: {
This is why main.cue writes $random.#Resource.random_integer: the resource type is a definition inside the provider it runs on.
Validate
Success! 2 resources, 0 data sources, 0 ephemeral, 0 function calls, 0 outputs across 2 providers.
validate evaluates the file and checks every field against the provider definitions. It starts no provider. It found two resources and two providers without a keyword that says so.
See what pravo sees
random_integer has the attributes id, keepers, max, min, result and seed. The file sets min, max and seed. Decide which fields pravo eval -e port prints as a type and not as a value.
Predict Which fields of port are not yet concrete?
// Identity: pravo treats a struct that carries these as a resource.
$kind: "resource"
$type: "random_integer"
$source: "registry.opentofu.org/hashicorp/random"
// The string representation of the integer result.
id: string | null
// The maximum inclusive value of the range.
max: 29999
// The minimum inclusive value of the range.
min: 20000
// The random integer result.
result: number | null
// A custom seed to always produce the same value.
seed: "web"
id and result are types. The provider computes them when it creates the object, so before apply they are not yet concrete. That is not an error. It is the state of a value that does not exist yet. B2 shows how a plan prints it.
That answers the question at the top. pravo eval started no provider and read no credential. The first three lines of the output are the identity. A struct with $kind: "resource" is a resource, wherever it sits in the file. $type is the resource type, and $source names the provider that runs it. The // lines are the provider's own documentation.
To look up what a resource accepts, ask for its definition. --required keeps the attributes that must be set:
// resource random_integer, from hashicorp/random
...
random_integer: {
...
// The maximum inclusive value of the range.
max: number
// The minimum inclusive value of the range.
min: number
}
Read the address
A plan names every resource that apply creates. Print only the header lines:
# port (random_integer) will be created
# web (local_file) will be created
Each header holds an address and, in parentheses, the type. port and web are paths in main.cue. The next lesson reads the whole plan.
Break it: a resource built outside a provider
The next file is rejected before any provider starts. It builds a resource from the bare definition, not through a provider struct. Write it beside main.cue:
package estate
import rnd "pravo.dev/providers/hashicorp/random"
other: rnd.#Provider.#Resource.random_pet & {length: 2}
Error: other was not built through a provider. Declare a provider, for example
$aws: awsp.#Provider & {...}
and build the resource through it: $aws.#Resource.random_pet & {...}
The path is other. The rule is that every resource is built through a provider struct that the file declares. The bare definition rnd.#Provider is not one. The error shows the form to use. Every command that loads the specification exits with code 6 when the specification is invalid.
Remove the file:
Success! 2 resources, 0 data sources, 0 ephemeral, 0 function calls, 0 outputs across 2 providers.
Break it: a misspelt attribute
This file is rejected too. filnename is not an attribute of local_file. Write it beside main.cue:
package estate
backup: $local.#Resource.local_file & {
filnename: "backup.conf"
content: "port = 0\n"
}
Error: evaluating .:
backup.filnename: field not allowed:
./typo.cue:4:2
Read it as path, rule, location. The path is backup.filnename. The rule is field not allowed. The definition of local_file is closed, as in A8, and lists the only attributes a struct may carry. The location is line 4, column 2 of typo.cue.
No provider was involved. validate reads the definitions that init wrote. Ask for the log and look for a provider process:
no provider started
A plan does start the providers. Remove the typo and count them:
2
What validate leaves to the provider
validate checks names, types and constraints. A provider also has rules of its own, such as a required attribute. The provider checks those when it plans, and validate does not start it.
package estate
lonely: $random.#Resource.random_integer & {min: 1}
Success! 3 resources, 0 data sources, 0 ephemeral, 0 function calls, 0 outputs across 2 providers.
Error: lonely: Missing Configuration for Required Attribute (at max): Must set a configuration value for the max attribute as the provider has marked it as required.
Refer to the provider documentation or contact the provider developers for additional information about configurable attributes that are required.
Exercise
Add a second port
Add two resources to main.cue. admin_port is a random_integer from 30000 to 30999 with the seed "admin". admin is a local_file named admin.conf that records the port as web does.
Then misspell max as mx in admin_port, run pravo validate, read the error, and undo the change.
When you are done, this must print ok:
pravo validate | grep -q 'Success! 4 resources' && echo ok
Hint 1
Copy the two resources and change the names, the numbers and the file name. Run pravo schema random_integer --required to see the attributes you must set.
Hint 2
admin interpolates admin_port.result. A misspelling in admin_port is reported at that interpolation, as invalid interpolation: field not allowed. The path in front of the message, admin_port.mx, names the typo. Break it: a misspelt attribute shows a clean example.
Solution
package estate
import (
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
)
$random: rnd.#Provider & {$version: "~> 3.6"}
$local: loc.#Provider
port: $random.#Resource.random_integer & {
min: 20000
max: 29999
seed: "web"
}
web: $local.#Resource.local_file & {
filename: "web.conf"
content: "port = \(port.result)\n"
}
admin_port: $random.#Resource.random_integer & {
min: 30000
max: 30999
seed: "admin"
}
admin: $local.#Resource.local_file & {
filename: "admin.conf"
content: "port = \(admin_port.result)\n"
}
Recap
- A provider is a struct built from the generated
#Provider. A resource is a struct built through a provider. - The address of a resource is its path, and a reference to another resource is the dependency.
pravo evalshows a resource andpravo validatechecks it before any provider starts. A resource built outside a provider and a misspelt attribute are refused.
Check yourself
Which fields of a resource does pravo read to learn what it is?
$kind, $type and $source. A struct that carries $kind: "resource" is a resource, wherever it sits.
Why does pravo validate reject filnename?
The generated definition of local_file is closed. A field it does not list is field not allowed.
Reference: Providers, Resources and addresses, pravo schema and pravo validate.
B2 reads the plan that this file produces.