§ B5 · Track B · pravo
Many resources from one rule
A comprehension produces resources. Each key becomes the last element of an address, so reordering the source replaces nothing.
On this page
Warm-up
Two questions from earlier lessons. Decide, then open the answer.
Predict You save a plan with -t env=prod. Can you apply it with -t env=dev?
No. A saved plan carries its own tags, and pravo refuses -t with a plan file (B4).
Predict The rule for name, svc in services { (name): ... } writes one field per entry. What does the label of each field hold?
The key of the entry in services. The parentheses make the label an expression (A10).
The problem
Three services need a configuration file each, and a port to put in it. You can write six resources, or you can write one rule and a list of services.
In HCL, count repeats a block, and each copy is named by its position.
variable "services" {
default = ["api", "web", "worker"]
}
resource "local_file" "conf" {
count = length(var.services)
filename = "${var.services[count.index]}.conf"
content = "name = ${var.services[count.index]}\n"
}
The copies are local_file.conf[0], local_file.conf[1] and local_file.conf[2]. Put "worker" first in the list and each position holds a different service. The addresses stay the same, and the services behind them change. for_each over a set names the copies by key and avoids this.
The idea
A comprehension in CUE builds the fields of a struct from another struct. The key of each source field becomes the name of the new field. The address of a resource is its path, so the key is the last element of the address. The order of the source is not part of the value. A struct has keys and no positions, so the stable form is the one you get by default. The unstable form needs a list, and the lesson shows what it costs.
Write the rule
Work in an empty directory. Three files. services.cue holds the data. rules.cue holds the rule. main.cue holds the providers.
package estate
import (
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
)
$random: rnd.#Provider
$local: loc.#Provider
package estate
services: {
api: {workers: 2}
web: {workers: 1}
worker: {workers: 4}
}
package estate
port: {
for name, _ in services {
(name): $random.#Resource.random_integer & {
min: 20000
max: 29999
seed: name
}
}
}
conf: {
for name, svc in services {
(name): $local.#Resource.local_file & {
filename: "\(name).conf"
content: "port = \(port[name].result)\nworkers = \(svc.workers)\n"
}
}
}
Read the rule.
for name, svc in servicesvisits each field ofservices.nameis the key andsvcis the value. A10 covers the syntax.(name): ...writes a field whose label is the value ofname. The parentheses mark the label as an expression.- Each new field holds a resource built through a provider, as in B1.
port[name]selects the port with the same key. That reference is the dependency of each file on its port.
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.
Read the addresses
Success! 6 resources, 0 data sources, 0 ephemeral, 0 function calls, 0 outputs across 2 providers.
# port.api (random_integer) will be created
# port.web (random_integer) will be created
# port.worker (random_integer) will be created
# conf.api (local_file) will be created
# conf.web (local_file) will be created
# conf.worker (local_file) will be created
Three services and two kinds of resource make six resources. Each address is the name of the field that holds the rule, then the key:
| Key | Ports | Files |
|---|---|---|
api |
port.api |
conf.api |
web |
port.web |
conf.web |
worker |
port.worker |
conf.worker |
The key reaches the resource in CUE as well:
"api.conf"
Apply the six resources and list what the state holds:
Apply complete! Resources: 6 added, 0 changed, 0 destroyed. [txn <id>]
conf.api
conf.web
conf.worker
port.api
port.web
port.worker
conf.api -> port.api
conf.web -> port.web
conf.worker -> port.worker
The graph has one edge per key. conf.api reads port.api and no other port.
Reorder the keys
Put worker first and api second. Nothing else changes.
Predict You write the services in another order in services.cue. How many resources does the plan replace?
None. The address of each resource is built from its key, and the keys have not changed. The order of the lines is not part of the value that pravo plans.
Check the answer. This file holds the same three services in a new order:
package estate
services: {
worker: {workers: 4}
api: {workers: 2}
web: {workers: 1}
}
No changes. The specification matches the state. 6 records were not read; run pravo drift to compare them with the infrastructure.
Graph: planned 0 of 6, 6 unchanged and skipped, 6 not read.
The plan reports No changes. and reaches no resource. Sort the services, group them or add a comment between them. The infrastructure does not move.
Break it: a list instead of a struct
A comprehension over a list builds a list. The address of an element is its index, written conf[0]. The next two files form a second root in the directory list. It has its own state and stays apart from the first.
package list
names: ["api", "web", "worker"]
package list
import loc "pravo.dev/providers/hashicorp/local"
$local: loc.#Provider
conf: [for name in names {
$local.#Resource.local_file & {
filename: "list-\(name).conf"
content: "name = \(name)\n"
}
}]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed. [txn <id>]
conf[0]
conf[1]
conf[2]
The addresses are conf[0], conf[1] and conf[2]. Now reorder the list. The file below is valid and is not what you meant:
package list
names: ["worker", "api", "web"]
# conf[0] (local_file) must be replaced
# conf[1] (local_file) must be replaced
# conf[2] (local_file) must be replaced
Plan: 3 to add, 0 to change, 3 to destroy.
Graph: planned 3 of 3, 3 refreshed.
Each address now holds a different service. The file names differ, so every file is replaced. The services did not change, and the plan replaces three files.
Pitfall
An index is an address. Generate resources from a struct whose keys are stable names. Use a list only when the position is the identity.
Exercise
Change the services
Add the service cache with two workers, and remove worker. Predict the Plan: line first. Then apply, and check that api and web were not touched.
When you are done, this must print ok:
pravo plan --detailed-exitcode >/dev/null && pravo state list | grep -qx conf.cache && ! pravo state list | grep -q worker && echo ok
Hint 1
A new key adds two resources, one port and one file. A removed key destroys two.
Hint 2
Read Graph: in the plan. It reaches the two new resources and skips the four that did not change. A10 explains why the keys stay stable.
Solution
package estate
services: {
api: {workers: 2}
web: {workers: 1}
cache: {workers: 2}
}
# port.cache (random_integer) will be created
# conf.cache (local_file) will be created
# conf.worker (local_file) will be destroyed
# port.worker (random_integer) will be destroyed
Plan: 2 to add, 0 to change, 2 to destroy.
Graph: planned 2 of 6, 4 unchanged and skipped, 2 removed from the specification, 2 refreshed, 4 not read.
Keys that are not identifiers
A key such as my-web is not a plain CUE identifier. The address quotes it. This runs in a third root, keys, with its own state.
package keys
import loc "pravo.dev/providers/hashicorp/local"
$local: loc.#Provider
conf: "my-web": $local.#Resource.local_file & {filename: "my-web.conf", content: "web\n"}
# conf."my-web" (local_file) will be created
The address is conf."my-web". Quote the whole address when you give it to a shell, for example pravo state show 'conf."my-web"'. A key is part of the address, so renaming a key creates a new resource and destroys the old one. B8 shows how to keep the object when you rename a key.
Clean up
Destroy complete! Resources: 0 added, 0 changed, 6 destroyed. [txn <id>]
Destroy complete! Resources: 0 added, 0 changed, 3 destroyed. [txn <id>]
Recap
- A comprehension writes one field per key, and a resource in that field has the address
field.key. - The order of the source does not change an address. A list uses the index as the address, so its order does.
- Adding or removing a key adds or destroys only the resources of that key.
Check yourself
What is the address of the file for the service web in rules.cue?
conf.web. The field conf holds the rule, and the key web is the last element.
Why does reordering a list replace resources and reordering a struct does not?
In a list the index is the address. In a struct the key is the address, and the key does not move.
Reference: Resources and addresses and Resources.
B6 packs resources into a module and passes providers to it.