Say in the plan which file a container would be recreated for
The plan says what an apply would change from the declaration and the record, before the machine is touched. The apply now recreates a container when the content of a file it reads at creation changed, and the plan said "check" for every recorded container — true, but a preview that hides the one step somebody asked about. So a recorded container whose record of what it read differs from what this apply will hand it — a plain file declared here, by its declared content; otherwise what this host last wrote at that path — is planned as an update naming the file, the same comparison applyContainer makes. What the record cannot settle stays a check: a file neither declared nor recorded is read from the machine by the apply, not by the plan; and a container with no record of what it read was labelled before the host kept that record and is accepted as it is. novox/hq 04-ISSUES/103, 104
This commit is contained in:
@@ -191,10 +191,68 @@ func planned(r declaration.Resource, d *declaration.Declaration, known store.Sta
|
||||
step.Verb, step.Why = "update", "the declaration changed since this host applied it"
|
||||
return step
|
||||
}
|
||||
if c, ok := r.(*declaration.Container); ok {
|
||||
if changed := readsChanged(c, d, known, was.Reads); len(changed) > 0 {
|
||||
step.Verb = "update"
|
||||
step.Why = "recreated: " + strings.Join(changed, ", ") + " changed since it was created"
|
||||
return step
|
||||
}
|
||||
}
|
||||
step.Verb, step.Why = "check", "recorded here; corrected if this machine drifted from it"
|
||||
return step
|
||||
}
|
||||
|
||||
// readsChanged is which of the files a container was created reading the apply will hand it
|
||||
// changed — the same comparison applyContainer makes (novox/hq 04-ISSUES/103), settled from the
|
||||
// declaration and the record alone.
|
||||
//
|
||||
// A file's digest is what this apply will record for it: a plain file declared here, by its
|
||||
// declared content; otherwise what this host last wrote there, under any id. A file neither
|
||||
// declares nor records — an env-file a predecessor left — is read by the apply from the machine,
|
||||
// which a plan does not do, so it stays a check. A container with no record of what it read was
|
||||
// labelled before the host kept that record and is accepted as it is, so it is a check too.
|
||||
func readsChanged(c *declaration.Container, d *declaration.Declaration, known store.State,
|
||||
wasReading map[string]string) []string {
|
||||
if len(wasReading) == 0 {
|
||||
return nil
|
||||
}
|
||||
willWrite := map[string]string{}
|
||||
for _, r := range d.Resources {
|
||||
if f, ok := r.(*declaration.File); ok {
|
||||
if want := wouldWrite(f); want != "" {
|
||||
willWrite[f.Path] = want
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only what it still reads: a file it was created reading and no longer names is a changed
|
||||
// declaration, not a changed file.
|
||||
stillReads := map[string]bool{}
|
||||
for _, path := range c.EnvFile {
|
||||
stillReads[path] = true
|
||||
}
|
||||
for _, v := range c.Volumes {
|
||||
if src := mountSource(v); strings.HasPrefix(src, "/") {
|
||||
stillReads[src] = true
|
||||
}
|
||||
}
|
||||
var changed []string
|
||||
for _, path := range sortedKeys(wasReading) {
|
||||
if !stillReads[path] {
|
||||
continue
|
||||
}
|
||||
now, settled := willWrite[path]
|
||||
if !settled {
|
||||
if f, recorded := known.At(string(declaration.TypeFile), path); recorded {
|
||||
now, settled = f.Wrote, true
|
||||
}
|
||||
}
|
||||
if settled && now != wasReading[path] {
|
||||
changed = append(changed, path)
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// wouldWrite is the digest a plain file would be recorded under, or empty where only the apply
|
||||
// can know: a sealed file, one with secrets in it, one written into, one carrying bytes.
|
||||
func wouldWrite(r declaration.Resource) string {
|
||||
|
||||
@@ -225,3 +225,39 @@ func TestAResourceRunInAHeldContainerIsPlannedAsItIsApplied(t *testing.T) {
|
||||
t.Errorf("planned %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPlanSaysAContainerIsRecreatedWhenAFileItReadsChanged(t *testing.T) {
|
||||
// The apply recreates a container when the content of a file it reads at creation changed
|
||||
// (novox/hq 04-ISSUES/103); the plan says so from the record alone — what the container was
|
||||
// created reading, against what this apply will write. And a container recorded before the
|
||||
// host kept that record is accepted, so it is a check, not an update.
|
||||
dir := t.TempDir()
|
||||
env := filepath.Join(dir, "forge.env")
|
||||
declare := func(port string) *declaration.Declaration {
|
||||
return trusted(t, `{"declaration":1,"resources":[
|
||||
{"id":"forge.env","type":"file","path":"`+env+`","content":"DATABASE_PORT=`+port+`\n"},
|
||||
{"id":"forge.server","type":"container","name":"forge","image":"`+pinned+`","env-file":["`+env+`"]}]}`)
|
||||
}
|
||||
created := digestOf("DATABASE_PORT=5432\n")
|
||||
known := store.State{}
|
||||
known.Record(store.Applied{ID: "forge.env", Type: "file", Target: env, Wrote: created})
|
||||
known.Record(store.Applied{ID: "forge.server", Type: "container", Target: "forge",
|
||||
Reads: map[string]string{env: created}})
|
||||
|
||||
if got := verbs(Plan(declare("5432"), known, store.OriginCarried)); got != "check forge.env, check forge.server" {
|
||||
t.Errorf("nothing changed and the plan says %q", got)
|
||||
}
|
||||
steps := Plan(declare("5433"), known, store.OriginCarried)
|
||||
if got := verbs(steps); got != "update forge.env, update forge.server" {
|
||||
t.Fatalf("the env-file changes and the plan says %q", got)
|
||||
}
|
||||
if !strings.Contains(steps[1].Why, env+" changed") {
|
||||
t.Errorf("the plan does not say which file: %+v", steps[1])
|
||||
}
|
||||
|
||||
// No record of what it read: labelled by an earlier host, accepted as it is.
|
||||
known.Record(store.Applied{ID: "forge.server", Type: "container", Target: "forge"})
|
||||
if got := verbs(Plan(declare("5433"), known, store.OriginCarried)); got != "update forge.env, check forge.server" {
|
||||
t.Errorf("a container with no record of what it read is planned as %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user