§ B8 · Track B · pravo
Renames, moves, imports
Keep the object of a resource when its address changes, and adopt an object that already exists.
On this page
You rename a resource in the file. In HCL the next plan destroys the old object and creates a new one. A moved block prevents that, and you keep it until every state has seen it. An object that exists and has no record needs an import block. Both blocks are separate from the resource, and both name addresses as text.
moved {
from = random_integer.server
to = random_integer.primary
}
import {
to = random_integer.dice
id = "5,1,10"
}
primary: $random.#Resource.random_integer & {
$from: "server"
min: 1
max: 99
}
dice: $random.#Resource.random_integer & {
$import: "5,1,10"
min: 1
max: 10
}
In pravo the address of a resource is the path where its struct sits. Rename the field and you have a new address. $from and $import are fields of the resource they describe.
An address is an identity
Work in an empty directory. This root has a number and a file that reads it.
package main
import (
loc "pravo.dev/providers/hashicorp/local"
rnd "pravo.dev/providers/hashicorp/random"
)
$local: loc.#Provider
$random: rnd.#Provider
server: $random.#Resource.random_integer & {min: 1, max: 99, seed: "s1"}
note: $local.#Resource.local_file & {
filename: "note.txt"
content: "server \(server.result)\n"
}
Configuration does not load yet; generating schemas for the declared providers first.
Created cue.mod/module.cue
Providers:
local = hashicorp/local v2.9.1
random = hashicorp/random v3.9.1
...
pravo has been initialised. Run "pravo plan" to see what it would do.
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <txn>]
note
server
The state holds two records, at the addresses server and note. Rename server to primary and update the reference. The object is the same. Only the name changes.
package main
import (
loc "pravo.dev/providers/hashicorp/local"
rnd "pravo.dev/providers/hashicorp/random"
)
$local: loc.#Provider
$random: rnd.#Provider
primary: $random.#Resource.random_integer & {min: 1, max: 99, seed: "s1"}
note: $local.#Resource.local_file & {
filename: "note.txt"
content: "server \(primary.result)\n"
}
Count the records in the state, and the addresses the specification now declares.
Predict What does pravo plan do with this rename?
The state has server, and the specification has primary. pravo plans to create primary and to destroy server. The file reads primary, so it is replaced as well.
...
# primary (random_integer) will be created
+ primary: {
+ id: (known after apply)
+ max: 99
+ min: 1
+ result: (known after apply)
+ seed: "s1"
}
# note (local_file) must be replaced
-/+ note: {
~ content: "server 3\n" -> (known after apply) # forces replacement
...
}
# server (random_integer) will be destroyed
- server: {
- id: "3"
- max: 99
- min: 1
- result: 3
- seed: "s1"
}
Plan: 2 to add, 0 to change, 2 to destroy.
Hint: primary looks like server under a new address; to keep the existing object, add $from: "server" to primary
Graph: planned 2 of 2, 1 removed from the specification, 2 refreshed.
The plan creates two resources and destroys two. A line starting with Hint says that primary looks like server under a new address. It appears when a plan creates one resource and destroys another of the same type with the same configuration.
Applying this plan destroys the number, creates another and replaces the file that reads it.
Keep the object with $from
Name the old address on the resource that has the new one.
package main
import (
loc "pravo.dev/providers/hashicorp/local"
rnd "pravo.dev/providers/hashicorp/random"
)
$local: loc.#Provider
$random: rnd.#Provider
primary: $random.#Resource.random_integer & {$from: "server", min: 1, max: 99, seed: "s1"}
note: $local.#Resource.local_file & {
filename: "note.txt"
content: "server \(primary.result)\n"
}
server: moved to primary
...
pravo will perform the following actions:
# server was moved to primary (record renamed; no change to the object)
Plan: 0 to add, 0 to change, 0 to destroy.
Graph: planned 0 of 2, 2 unchanged and skipped, 2 not read, 1 moved.
The plan renames the record. It changes nothing in the world: 0 to add, 0 to change, 0 to destroy. The move happens as the first step of the plan, so the state already holds the new address.
Note
A plan that carries out a $from or an $import writes to the state. No object changes, and only the records move.
note
primary
...
No changes. The specification matches the state. 2 records were not read; run pravo drift to compare them with the infrastructure.
Graph: planned 0 of 2, 2 unchanged and skipped, 2 not read.
A second plan finds nothing to do. The $from can stay in the file or go. Once the old address is gone from the state, the field does nothing.
A move keeps the type. A $from that names a record of another type is refused.
Move a record to a different type
Replace the root with one resource that claims the record of the number as a file.
package main
import loc "pravo.dev/providers/hashicorp/local"
$local: loc.#Provider
copy: $local.#Resource.local_file & {$from: "primary", filename: "copy.txt", content: "x\n"}
Error: $from primary -> copy: the record is a managed random_integer but copy is declared as a managed local_file; the type must stay the same
The plan stops before it plans anything. The record of primary is a random_integer, and copy is a local_file.
Move a group
A struct that holds resources moves as one. Every record below the old path moves to the same place below the new one. A definition is closed, so it must accept the field. Embed pravo.#Movable.
package main
import (
"pravo.dev/pravo"
loc "pravo.dev/providers/hashicorp/local"
rnd "pravo.dev/providers/hashicorp/random"
)
$local: loc.#Provider
$random: rnd.#Provider
primary: $random.#Resource.random_integer & {$from: "server", min: 1, max: 99, seed: "s1"}
note: $local.#Resource.local_file & {
filename: "note.txt"
content: "server \(primary.result)\n"
}
#Naming: {
pravo.#Movable
first: $random.#Resource.random_integer & {min: 1, max: 9, seed: "first"}
second: $random.#Resource.random_integer & {min: 1, max: 9, seed: "second"}
}
names: #Naming
...
Plan: 2 to add, 0 to change, 0 to destroy.
Graph: planned 2 of 4, 2 unchanged and skipped, 2 not read.
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <txn>]
names.first
names.second
note
primary
Rename the instance from names to ids and name the address its records have now.
package main
import (
"pravo.dev/pravo"
loc "pravo.dev/providers/hashicorp/local"
rnd "pravo.dev/providers/hashicorp/random"
)
$local: loc.#Provider
$random: rnd.#Provider
primary: $random.#Resource.random_integer & {$from: "server", min: 1, max: 99, seed: "s1"}
note: $local.#Resource.local_file & {
filename: "note.txt"
content: "server \(primary.result)\n"
}
#Naming: {
pravo.#Movable
first: $random.#Resource.random_integer & {min: 1, max: 9, seed: "first"}
second: $random.#Resource.random_integer & {min: 1, max: 9, seed: "second"}
}
ids: #Naming & {$from: "names"}
names.first: moved to ids.first
names.second: moved to ids.second
...
pravo will perform the following actions:
# names.first was moved to ids.first (record renamed; no change to the object)
# names.second was moved to ids.second (record renamed; no change to the object)
Plan: 0 to add, 0 to change, 0 to destroy.
Graph: planned 0 of 4, 4 unchanged and skipped, 4 not read, 2 moved.
Both records moved. This is how you rename a module instance, or move resources into one.
Break it: a definition that is closed
Leave pravo.#Movable out of the definition. A small root of its own shows the refusal. The block carries status=rejected.
package closed
import rnd "pravo.dev/providers/hashicorp/random"
$random: rnd.#Provider
#Naming: {
first: $random.#Resource.random_integer & {min: 1, max: 9, seed: "first"}
}
ids: #Naming & {$from: "names"}
Error: evaluating ./closed:
ids.$from: field not allowed:
./closed/main.cue:11:17
A definition is closed, as in A8. $from is a field the definition does not declare. pravo.#Movable declares it.
Adopt an object with $import
An object that exists can be brought under management without being created again. Put the provider's id for it in $import. For random_integer the id is the result, the minimum and the maximum, joined by commas.
package main
import (
"pravo.dev/pravo"
loc "pravo.dev/providers/hashicorp/local"
rnd "pravo.dev/providers/hashicorp/random"
)
$local: loc.#Provider
$random: rnd.#Provider
primary: $random.#Resource.random_integer & {$from: "server", min: 1, max: 99, seed: "s1"}
note: $local.#Resource.local_file & {
filename: "note.txt"
content: "server \(primary.result)\n"
}
#Naming: {
pravo.#Movable
first: $random.#Resource.random_integer & {min: 1, max: 9, seed: "first"}
second: $random.#Resource.random_integer & {min: 1, max: 9, seed: "second"}
}
ids: #Naming & {$from: "names"}
dice: $random.#Resource.random_integer & {min: 1, max: 10, $import: "5,1,10"}
...
dice: Importing from id "5,1,10"...
...
pravo will perform the following actions:
# dice was imported from id "5,1,10" and matches the specification
Plan: 0 to add, 0 to change, 0 to destroy.
Graph: planned 1 of 5, 4 unchanged and skipped, 4 not read, 1 imported.
The provider read the object, and the result became the record for dice. The plan compared the record with the specification. They match, so there is nothing to do. With max: 20 the plan reports the import and a replacement, because a new max forces one.
The record has no application time and no configuration hash. It was never applied by pravo.
// dice (provider registry.opentofu.org/hashicorp/random v3.9.1 as random, schema version 0, revision 1)
// refreshed 0s ago, applied never, config hash none
{
id: "5"
keepers: null
max: 10
min: 1
result: 5
seed: null
}
A later plan does not import again, because the record exists. Leave $import in the file. The id format is whatever the provider documents for the resource type. Only managed resources can be imported.
pravo import does the same from the command line. Declare the resource first, without $import, and pass the address and the id.
package main
import (
"pravo.dev/pravo"
loc "pravo.dev/providers/hashicorp/local"
rnd "pravo.dev/providers/hashicorp/random"
)
$local: loc.#Provider
$random: rnd.#Provider
primary: $random.#Resource.random_integer & {$from: "server", min: 1, max: 99, seed: "s1"}
note: $local.#Resource.local_file & {
filename: "note.txt"
content: "server \(primary.result)\n"
}
#Naming: {
pravo.#Movable
first: $random.#Resource.random_integer & {min: 1, max: 9, seed: "first"}
second: $random.#Resource.random_integer & {min: 1, max: 9, seed: "second"}
}
ids: #Naming & {$from: "names"}
dice: $random.#Resource.random_integer & {min: 1, max: 10, $import: "5,1,10"}
coin: $random.#Resource.random_integer & {min: 0, max: 1}
coin: Importing from ID "1,0,1"...
coin: Import complete. Run "pravo plan" to compare it with the configuration.
coin
dice
ids.first
ids.second
note
primary
The specification carries $import, so a reviewer sees it in the diff. The command changes the state at once and leaves no trace in the files. Prefer $import.
Edit the state directly
Two commands change the state without a plan.
| Command | Effect |
|---|---|
pravo state mv <from> <to> |
Renames one record. |
pravo state rm <addr> |
Forgets a record. The object stays. |
Both act at once and nobody reviews them. $from does the same work in a plan. Rename coin to flip and back to see the effect.
Moved coin to flip
dice
flip
ids.first
ids.second
note
primary
Moved flip to coin
Exercise
Rename without recreating
Rename note to readme and keep the file note.txt. The plan must say that the record was renamed and must change nothing.
Hint 1
Put $from on the resource that has the new name. It holds the old address as a string.
Hint 2
The plan prints moved to for each record it renames. See Moves and imports.
Solution
package main
import (
"pravo.dev/pravo"
loc "pravo.dev/providers/hashicorp/local"
rnd "pravo.dev/providers/hashicorp/random"
)
$local: loc.#Provider
$random: rnd.#Provider
primary: $random.#Resource.random_integer & {$from: "server", min: 1, max: 99, seed: "s1"}
readme: $local.#Resource.local_file & {
$from: "note"
filename: "note.txt"
content: "server \(primary.result)\n"
}
#Naming: {
pravo.#Movable
first: $random.#Resource.random_integer & {min: 1, max: 9, seed: "first"}
second: $random.#Resource.random_integer & {min: 1, max: 9, seed: "second"}
}
ids: #Naming & {$from: "names"}
dice: $random.#Resource.random_integer & {min: 1, max: 10, $import: "5,1,10"}
coin: $random.#Resource.random_integer & {min: 0, max: 1}
note: moved to readme
...
# note was moved to readme (record renamed; no change to the object)
Plan: 0 to add, 0 to change, 0 to destroy.
Graph: planned 2 of 6, 4 unchanged and skipped, 2 refreshed, 4 not read, 1 moved.
Clean up
Destroy complete! Resources: 0 added, 0 changed, 6 destroyed. [txn <txn>]
Recap
- An address is the path where a struct sits, and it is the identity of a resource. A rename without
$fromdestroys and creates. $fromnames the old address. On a struct that holds resources it moves every record below it.$importadopts an existing object by its provider id.pravo importdoes the same from the command line.
Check yourself
What does a plan print when it creates one resource and destroys another of the same type and configuration?
A Hint line that names the new address, the old one, and the $from that keeps the object.
Why does a definition reject $from unless it embeds pravo.#Movable?
A definition is closed. It accepts only the fields it declares, and pravo.#Movable declares $from.
To do this on your own project, use the how-to Rename, move and adopt resources. B9 finds the changes that someone made outside pravo. Migrate uses $from to point converted state at new addresses.